Open chinedufn opened 2 years ago
Marinated on this a bit.
I'm liking the idea of the user bringing their own runtime (by adding an annotation at either the module, extern or individual function level as illustrated in this issues body).
Since the vast majority of users won't need to customize their own runtime, we can consider providing rt-multi
and rt-single
feature flags.
rt-multi
would lazily initialize a multi threaded async runtime (basically what we already do today). rt-single
would do the same thing but with a single threaded runtime.
// Assuming the `rt-single` feature flag was enabled.
#[swift_bridge(async_runtime = swift_bridge::async::RuntimeSingleThreaded)]
mod ffi {
extern "Rust" {
async fn foo();
}
}
These are just some ideas.. I generally dislike the idea of including too many batteries here. It might make sense to not provide any default runtimes and just push everything into user-land.
And then just have our guides / docs show how to initialize your own runtime easily.
struct MyRuntime;
impl swift_bridge::async::AsyncRuntime for MyRuntime {
fn spawn(&self, task: swift_bridge::async::AsyncFnToSpawn) {
// ...
}
}
This all needs more design thinking.. Just jotting down thoughts as they come to me.
An open question is how libraries should handle async runtimes.
Say you have a library my-api-library
that exposes async functions to Swift.
Then my-binary
crate depends on my-api-library
.
How does my-api-library
spawn async tasks? Needing every library to bundle its own async runtime would be wasteful.
Perhaps the library would use a "runtime" that just buffered tasks, and then the runtime in the final binary would be responsible for running those tasks. Something along the lines of:
// Trait for defining an async runtime
use swift_bridge::async::AsyncRuntime;
pub struct LibraryRuntime {
buffer: Vec<AsyncFnToSpawn>,
real_runtime: Option<Box<dyn AsyncRuntime>>
}
impl LibraryRuntime {
fn set_runtime (&self, runtime: Box<dyn AsyncRuntime>) {
for task in self.buffer {
runtime.spawn(task);
}
self.buffer.resize(0);
self.real_runtime = Some(runtime);
}
}
impl AsyncRuntime for MyRuntime {
fn spawn(&self, task: swift_bridge::async::AsyncFnToSpawn) {
if let Some(runtime) = &self.real_runtime {
runtime.spawn(task);
} else {
// Would need interior mutability...
self.buffer.push(task);
}
}
}
And then the final executable would use my_api_library::LibraryRuntimeInstance
and call set_runtime
on it.. Where LibraryRuntimeInstance
is just a lazily initialized LibraryRuntime
.
And my_api_library
would have its bridge look like:
#[swift_bridge(async_runtime = LibraryRuntimeInstance)]
mod ffi {
extern "Rust" {
async fn foo();
}
}
With this sort of setup.. swift-bridge
could provide the "LibraryRuntime" (with a better name...) that libraries would depend on.
I've papered over a lot of details (i.e. a library A that depends on a library B. Library A would need to provide a runtime to library B.. but that should be fine.. it would just pass down its own LibraryRuntime
into library B's LibraryRuntime
.) .. but in general this could work.
Would just need to feel it out.
Related #119
Right now we lazily create an async runtime whenever you first call an async Rust function from Swift.
https://github.com/chinedufn/swift-bridge/blob/72c2fa5f2214e0845d9b3fa29b56ffbf94a97c68/src/async_support.rs#L7-L16
Users currently have no control over the async runtime. We chose this approach as a quick way to quickly get async functions working, but now that we have a few users it's time to revisit it.
This issue was inspired by a comment https://github.com/chinedufn/swift-bridge/issues/50#issuecomment-1104625311 showing an error due to our current async implementation expecting futures to be
Send
.This issue is dedicated to thinking through how much control we want to give users over the async runtime.
Do we want users to be able to supply their own async runtime?
Do we want to just provide one multi threaded and one single threaded async runtime and give users the ability to choose which one to use? For example:
Or some other approach?
This needs some thinking. We should have a good sense of what people need to be able to do.. and then use that to inform our design.
I generally lean towards giving users as much flexibility as possible.. which has me leaning towards first exploring approaches where users can supply their own async runtimes.. but yeah let's investigate all of this a bit more.