mozilla / uniffi-rs

a multi-language bindings generator for rust
https://mozilla.github.io/uniffi-rs/
Mozilla Public License 2.0
2.68k stars 219 forks source link

Async runtime for function binding defined in UDL file #2219

Open lyzkov opened 4 weeks ago

lyzkov commented 4 weeks ago

I'm migrating from macros to UDL to decrease build time.

Is there any way to specify async runtime for an asynchronous function that has its definition in UDL marked with [Async] the same way I can do it with proc macro #[uniffi::export(async_runtime = "tokio")]?

[Error]
enum PingError {
  "IoError",
  "ParseError",
  "CustomError",
};

namespace ping_example {
  [Throws=PingError, Async]
  void start_listening();

  [Throws=PingError, Async]
  void dial_peer(string address);
};

From lib.rs:

/// Starts listening on all interfaces and a random, OS-assigned port.
pub async fn start_listening() -> Result<(), PingError> {
    // (...)
}

/// Dials the peer identified by the given multi-address.
pub async fn dial_peer(address: String) -> Result<(), PingError> {
    // (...)
}

Without async runtime specified I'm getting a runtime error:

Caught a panic calling rust code: "there is no reactor running, must be called from the context of a Tokio 1.x runtime"
mhammond commented 4 weeks ago

I'm migrating from macros to UDL to decrease build time.

That's interesting!

Re your question, I think that's not possible, although I believe it would not be particularly difficult to add.

lyzkov commented 4 weeks ago

Re your question, I think that's not possible, although I believe it would not be particularly difficult to add.

I've started implementing the AsyncRuntime= attribute similar to Throws= but being new to Rust, I feel like a child lost in the darkness. I've succeeded in parsing a new UDL attribute, but I have no idea how to scaffold functions, methods, and constructors into future expressions of tokio. 🍑

If it comes to build time, it is not a big deal since UDL speeds up only clean build in my configuration. uniffi-bindgen in procmacro mode requires building dynamic library before generating bindings, and static library compilation for specific target must be repeated after then. As such, I can compile and test cargo package in an external editor first while working on Rust code before integrating uncleaned build into Swift framework. Nonetheless, it would be super nice to have fully integrated environment with incremental compilations for foreign targets. πŸ˜Άβ€πŸŒ«οΈπŸ•ŠοΈ

PS. What uniffi::export_for_udl macro is for? I can set async_runtime = "tokio" attribute for that macro, but I'm getting error[E0425]: cannot find function 'some_fun' in this scope whenever I apply it to any function, regardless of its signature's presence in the UDL file.

PS2. By so-called scaffolding I mean exporting pub fun, struct, enum as an c interface for foreign targets. As I understand: Semicompilation to C grants interoperability for FFI.

PS3. But what about UDL manifests for Xcompilation? Can UDLs be nested across cargo workspace for libraries that cross compile?

EDIT: I've used async-std as an alternative for tokio. We will think about adding missing implementation for UDL later if that runtime is more benefitable as someone suggested on rust-libp2p.

mhammond commented 2 weeks ago

Sorry for no response, I just got back from a break! I also misunderstood the problem when I said "not particularly difficult" :) This will require some fiddling in that macro code.

I don't understand the motivation.

uniffi-bindgen in procmacro mode requires building ...

uniffi-bindgen doesn't have a "procmacro mode". It does have a UDL mode, which generate .rs code - but that generated code uses the procmacros. I can't see how a UDL project could be faster to build than one without a UDL.

lyzkov commented 2 weeks ago

Forgive me for having little knowledge of crosscompilation. I'm guessing a lot without knowing, like a fool. ^^

In all the tutorials I've found, proc macros require me to compile the whole project to the default/native target before generating bindings for the foreign target. It is because of generate --library command that requires the dynamic binary file (dylib) to operate. Then, after generating bindings, I have to recompile the library from scratch because I need a static library for my foreign target.

With UDL, I can build my static library for foreign targets first and then generate bindings from .udl files without recompiling the library for the native target.

Is it possible to use a static library cross-compiled for the foreign target as a parameter for generate --library argument? This would fix my issue.

mhammond commented 2 weeks ago

I see - although I guess I'm a little surprised you can't generate the bindings from the foreign target - maybe that's a better thing to lean in to? The UDL path will also bite you if you ever want to end up with multiple crates sharing uniffi types.

lyzkov commented 2 weeks ago

UDL path feels coarse, especially for the beginner, because there is hardly any support from the UniFFI parser in debugging mistakes in UDL syntax. But I have a lot of determination to go through.

My project is still in the proof-of-concept phase. The sooner it fails, the better decision I'll make for further development. Till now, I've been building single crate packages from sample projects, but that can change soon. Are proc macros better suited for multi-crate interoperability?

mhammond commented 2 weeks ago

Are proc macros better suited for multi-crate interoperability?

It's more like "multi-crate interoperability typically requires generating the bindings from a built library instead of from a single UDL". It should be possible to change this so you can supply all the UDL files, but I don't think that's implemented. The issue isn't proc-macros vs UDL, because UDL is largely implemented by leaning on proc-macros.

mhammond commented 2 weeks ago

Back to the issue :) This is quite difficult due to only the .rs generation needing to know the runtime. It might be easier to have an option to allow all UDL async functions to specify the tokio runtime - that would be less intrusive (but still non-trivial - I think we'd want it specified in uniffi.toml, but the .rs code generation doesn't seem to read that) and might handle most use-cases.

setoelkahfi commented 2 weeks ago

I have a similar issue. So, for now, does it mean this kind of function signature async fn api_call() -> Result<u64, ErrorResponse> can only be exported through the proc-macro syntax?

lyzkov commented 2 weeks ago

I have a similar issue. So, for now, does it mean this kind of function signature async fn api_call() -> Result<u64, ErrorResponse> can only be exported through the proc-macro syntax?

You can use async-std runtime with both UDL and proc macros but tokio runtime currently works only with proc macros.