Interest in non-async drivers in libtock-rs
gendx opened this issue · comments
In OpenSK, we've successfully used the pre-async
version of libtock-rs drivers. Although there have been substantial changes in libtock-rs since the introduction of the new async
runtime, we've migrated the drivers to work on top of the current libtock-rs/core
.
These libtock-drivers are derived from the pre-async
libtock-rs, now adapted to work with changes that have affected libtock-rs/core
(e.g. the new TockResult
, the callback subscription/consumer APIs).
Is there any interest in having these forked drivers back in libtock-rs, i.e. having support for both async
and non-async
drivers in mainline libtock-rs? I think this would be easier to maintain by avoiding duplicate work, and would benefit users of libtock-rs
for whom the async
runtime doesn't fit (e.g. is too bulky).
Also, how would this interplay with "libtock-platform" (#217)?
The support for synchronuous subscriptions is still there, or am I missing something?
It seems that some drivers have migrated to the async
style, while others are indeed still synchronous, which maybe drove the confusion.
For example, the RNG driver only has an async
API:
Lines 26 to 41 in 05ac22a
whereas the buttons driver is only synchronous:
Lines 65 to 75 in 05ac22a
Also, a main blocker for building applications without async
runtime is that yieldk_for
was removed, as mentioned in the CHANGELOG.
So I guess my question is more:
- Should there be separate folders for
sync/
drivers andasync/
drivers? - Should all drivers implement both styles (in the same file) - or be able to implement both styles depending on how much resources contributors are willing to provide?
- Likewise, should there be separate folders for
examples/sync/
andexamples/async/
? (I assume there are no more synchronous examples now thatyieldk_for
has been removed)
Also, how would this interplay with "libtock-platform" (#217)?
For libtock_platform
, I intend for all drivers to be written in an asynchronous manner. I am developing traits to represent the asynchronous implementation. There are a lot of details and complications in flux (such as support for calling through zero-sized pointer types and mitigations against accidental recursion), but conceptually the traits are as follows:
// AsyncCall is implemented by types that provide asynchronous APIs.
pub trait AsyncCall<Request, SyncResponse> {
// Starts an asynchronous operation. Callbacks will be delivered to a `Callback`.
// Simple asynchronous operations ("do a thing and tell me when it is done") will
// receive one callback, while more complex operations ("start a recurring timer")
// may cause multiple callbacks. Therefore, implementers of `AsyncCall` must
// document when they will make callbacks after `start` has been called.
fn start(&self, request: Request) -> SyncResponse;
}
// Callback is implemented by clients of asynchronous APIs.
pub trait Callback<AsyncResponse> {
// Called when an asynchronous operation completes.
fn callback(&self, response: AsyncResponse);
}
Based on these traits, we can then develop a generic synchronous adapter to the asynchronous APIs:
pub struct SyncAdapter<Request, SyncResponse, AsyncResponse, Driver: AsyncCall<Request, SyncResponse>> {
...
}
impl<...> Callback<AsyncResponse> for SyncAdapter<...> { ... }
impl<...> SyncAdapter<...> {
// Calls into the driver to start the operation. This will not block.
pub fn start(&self, request: Request) -> SyncResponse { ... }
// Waits until a callback is received from the driver.
pub fn wait(&self) -> AsyncResponse { ... }
}
It may be possible to provide a single method that does start
and wait
if SyncResponse
is a Result
, which would be more convenient for working with simple asynchronous APIs.
Using this design, we can avoid maintaining parallel async + sync driver implementations. We may want to provide nicer wrappers around SyncAdapter
for each API, but it's unclear yet whether doing so is feasible (dependency injection and interoperability between async and sync code make this a nontrivial problem).
I guess the a sync version of the rng can be easily restored. Moreover, yieldk_for
is no syscall on its own but is just yield
+ waiting for a condition (use yield
+ a cell). While yield
is now declared unsafe for good reasons it can still be used.
FWIW, this is basically the libtock-c approach. It provides either raw yield
and APIs built on top (async) or yield_for
and APIs built on top (sync).
Things tend to light on fire if people try to mix them (as sync APIs assume that nothing (but other sync routine flag-setting) happens while they wait. In Rust, it'd probably be nicer to expose these as more formally mutually exclusive if you go that route.
Things tend to light on fire if people try to mix them (as sync APIs assume that nothing (but other sync routine flag-setting) happens while they wait. In Rust, it'd probably be nicer to expose these as more formally mutually exclusive if you go that route.
Ultimately, I think it would be good to allow mixing sync and async code in a single app, both to improve library reusability (you can write an async library and use it in a sync app), and to allow migrations between API styles (e.g. if an app that was originally synchronous suddenly needs to become asynchronous). There need to be clear boundaries, however. Async callbacks should not call into synchronous code, as doing so is going to cause surprising reentrancy.
Ultimately, in libtock_platform
, main()
will be synchronous. Synchronous routines will be able to call into other synchronous routines without problems, as well as start or wait for asynchronous operations. The boundaries will need to be clear. We can probably use some zero-sized types to help with this. For example, functions that can only be called in synchronous context can take a SyncContext
type to make programmers second-guess calling them from async code. In my libtock_platform
prototype, asynchronous callbacks already require a CallbackContext
instance that can only be obtained by callback functions.
I'll have to think a bit more about how Futures work in that model.
Ok, then let's wait for some progress on the libtock_platform
prototype.
Given that all of the libtock-rs drivers are currently being rewritten as part of the transition to Tock 2.0, and that libtock-rs does not currently include futures support, I think that this issue is no longer relevant.