Async!
A surprisingly large amount of cool stuff in Rust uses async. That's an argument we should probably avoid unless everyone's feeling fighty!
I recently worked on a project that wanted to use LanceDb (a vector embedding database) in C#. C# doesn't have the greatest FFI story, but it does have great async. Unfortunately, it's not the same async Rust uses - so you can't just magically call async functions. There really isn't a great story for creating bindings other than the C ABI, either. We're not going to look directly at that project (it's huge, but it's at https://github.com/thebracket/LanceDbCSharp/ if you're bored). But we can look at how it works - and how you can use it to bridge the FFI gap and still use async.
So there's an "external" set of functions:
- When you make a
Connection, if the runtime isn't initialized it lazilly spawns a thread and launches Tokio on it.- It's fun to make sure that Tokio is ready, so a "oneshot" channel calls back to say "I'm here!".
- We just hand out connection handles (id numbers) and keep the connections in a state table.
- When you try to perform a database operation with the connection:
- You call a function in the
external(C ABI). - The function puts everything you want to do into an enum.
- This includes callbacks:
- For function completion.
- For passing results back to the client when they are available.
- This includes callbacks:
- That enum is passed into Tokio as a channel call.
- A big
matchstatement spawns tokio tasks to handle the actual call. - Each task returns results by calling the provided callback. Rust owns the data - C# has to copy it (in this case that's unavoidable anyway, the format has to be marshalled).
- When the function finishes, it replies on a
oneshotto the calling function inexternal- which can then return.
- You call a function in the
It's not perfect, but it works---and you can use this pattern. Meta even use it with their async C++ and async Rust to bridge the divide between the two. There was a talk on that here last year.