Created
December 2, 2024 00:50
-
-
Save jberkenbilt/b6350fef1a974ef613a3091147ef587d to your computer and use it in GitHub Desktop.
Go to Rust: final code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! This is an internal implementation of sample API. The | |
//! implementation pretends to make network calls and accesses locked | |
//! data. It is wrapped by a function-based API that operates a | |
//! singleton. | |
use base::{AsyncRwLock, LockBox, Runtime}; | |
use implbox::ImplBox; | |
use std::error::Error; | |
use std::marker::PhantomData; | |
use std::ops::DerefMut; | |
#[derive(Default)] | |
struct ReqData { | |
seq: i32, | |
last_path: String, | |
} | |
pub struct Controller<RuntimeT: Runtime> { | |
req_data: ImplBox<LockBox<ReqData>>, | |
_r: PhantomData<RuntimeT>, | |
} | |
impl<RuntimeT: Runtime> Default for Controller<RuntimeT> { | |
fn default() -> Self { | |
Self { | |
req_data: RuntimeT::box_lock(Default::default()), | |
_r: Default::default(), | |
} | |
} | |
} | |
impl<RuntimeT: Runtime> Controller<RuntimeT> { | |
pub fn new() -> Self { | |
Default::default() | |
} | |
fn req_data(&self) -> &(impl AsyncRwLock<ReqData> + '_) { | |
RuntimeT::unbox_lock(&self.req_data) | |
} | |
async fn request(&self, path: &str) -> Result<(), Box<dyn Error + Sync + Send>> { | |
let mut lock = self.req_data().write().await; | |
let ref_data: &mut ReqData = lock.deref_mut(); | |
ref_data.seq += 1; | |
// A real implementation would make a network call here. Call await to make this | |
// non-trivially async. | |
async { | |
ref_data.last_path = format!("{path}&seq={}", ref_data.seq); | |
} | |
.await; | |
Ok(()) | |
} | |
/// Send a request and return the sequence of the request. | |
pub async fn one(&self, val: i32) -> Result<i32, Box<dyn Error + Sync + Send>> { | |
if val == 3 { | |
return Err("sorry, not that one".into()); | |
} | |
self.request(&format!("one?val={val}")).await?; | |
Ok(self.req_data().read().await.seq) | |
} | |
/// Send a request and return the path of the request. | |
pub async fn two(&self, val: &str) -> Result<String, Box<dyn Error + Sync + Send>> { | |
self.request(&format!("two?val={val}")).await?; | |
Ok(self.req_data().read().await.last_path.clone()) | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
use runtime_tokio::TokioRuntime; | |
#[tokio::test] | |
async fn test_basic() { | |
let c = Controller::<TokioRuntime>::new(); | |
assert_eq!(c.one(5).await.unwrap(), 1); | |
assert_eq!( | |
c.one(3).await.err().unwrap().to_string(), | |
"sorry, not that one" | |
); | |
assert_eq!(c.two("potato").await.unwrap(), "two?val=potato&seq=2"); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! This is a simple function-based wrapper around [Controller] that | |
//! operates on a singleton. You must call [init] first, and then you | |
//! can call the other functions, which call methods on the singleton. | |
use controller::Controller; | |
use runtime_tokio::TokioRuntime; | |
use std::error::Error; | |
use std::future::Future; | |
use std::sync::{LazyLock, RwLock}; | |
struct Wrapper { | |
rt: tokio::runtime::Runtime, | |
controller: RwLock<Option<Controller<TokioRuntime>>>, | |
} | |
static CONTROLLER: LazyLock<Wrapper> = LazyLock::new(|| Wrapper { | |
rt: tokio::runtime::Builder::new_current_thread() | |
.enable_all() | |
.build() | |
.unwrap(), | |
controller: Default::default(), | |
}); | |
// We want to create a dispatcher that blocks on an async method call. | |
// At the time of this writing (latest nightly rust = 1.84), async | |
// closures are not stable, but with the `async_closure` feature and a | |
// nightly build, this solution, using `async FnOnce` (or | |
// `AsyncFnOnce` -- it is not yet determined which syntax will win) | |
// and a higher-ranked trait bound, works: | |
// fn run_method<ArgT, ResultT, FnT>( | |
// f: FnT, | |
// arg: ArgT, | |
// ) -> Result<ResultT, Box<dyn Error + Sync + Send>> | |
// where | |
// FnT: async FnOnce(&Controller, ArgT) -> Result<ResultT, Box<dyn Error + Sync + Send>>, | |
// // OR: | |
// // FnT: std::ops::AsyncFnOnce(&Controller, ArgT) -> Result<ResultT, Box<dyn Error + Sync + Send>>, | |
// { | |
// let lock = CONTROLLER.controller.read().unwrap(); | |
// let Some(controller) = &*lock else { | |
// return Err("call init first".into()); | |
// }; | |
// CONTROLLER.rt.block_on(f(controller, arg)) | |
// } | |
// For more information about that, see | |
// - https://blog.rust-lang.org/inside-rust/2024/08/09/async-closures-call-for-testing.html | |
// - https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Async.20closures.20bounds.20syntax | |
// | |
// In the meantime, we can try the standard workaround of specifying | |
// the function type and the future type as two separate generic | |
// types, as in this: | |
// fn run_method<ArgT, ResultT, FnT, Fut>( | |
// f: FnT, | |
// arg: ArgT, | |
// ) -> Result<ResultT, Box<dyn Error + Sync + Send>> | |
// where | |
// FnT: FnOnce(&Controller, ArgT) -> Fut, | |
// Fut: Future<Output=Result<ResultT, Box<dyn Error + Sync + Send>>>, | |
// { | |
// let lock = CONTROLLER.controller.read().unwrap(); | |
// let Some(controller) = &*lock else { | |
// return Err("call init first".into()); | |
// }; | |
// CONTROLLER.rt.block_on(f(controller, arg)) | |
// } | |
// This doesn't work. We get an error on the method calls that "one | |
// type is more general than the other" with a suggestion of using a | |
// higher-ranked trait bound. So what's the actual problem? | |
// | |
// Our dispatcher has three lifetimes: | |
// - The outer lifetime, which is the default lifetime of references | |
// passed into the dispatcher | |
// - The lifetime of the controller object, which is shorter than the | |
// outer lifetime since the controller is a reference to the item | |
// inside the mutex | |
// - The lifetime captured by the future. | |
// | |
// fn dispatcher() { <-+ | |
// let fut = obj.method(arg); | | |
// ^ ^ | | |
// | +---- object that contains the method '2 |-- outer '1 | |
// +---------- future '3 | | |
// } <-+ | |
// | |
// As written, the lifetime of the `Controller` arg to `f` has the | |
// outer lifetime '1, but the controller doesn't live that long | |
// because it is actually created locally inside the call to the | |
// dispatcher. We need to use a higher-ranked trait bound (HRTB) to | |
// disconnect the lifetime of the controller from the outer lifetime. | |
// The problem is that we can't use a higher-ranked trait bound for | |
// `FnT` because we need `FnT` and `Fut` to share a lifetime. We want | |
// something like this: | |
// | |
// for <'a> { | |
// FnT: FnOnce(&Controller, ArgT) -> Fut, | |
// Fut: Future<Output=Result<ResultT, Box<dyn Error + Sync + Send>>>, | |
// } | |
// | |
// but there is no such syntax. So how can we create a higher rank | |
// trait bound that applies to both trait bounds? | |
// | |
// The solution is to create a custom trait that extends FnOnce and | |
// has an associated type that carries the Future's output type. If we | |
// put a lifetime on that trait, it will apply to the whole thing. | |
// Then we can use HRTB with that trait. | |
// | |
// The MethodCaller trait does not other than to apply the lifetime | |
// associated with the trait to the controller and tie it to the | |
// future. Since we need a concrete implementation, we provide a | |
// trivial blanket implementation that just includes a parameter with | |
// the same bounds as the associated type and then uses it as the | |
// associated type. Now we can attach our HRTB to a parameter bound by | |
// _this_ trait, and the lifetime will apply to the controller and the | |
// future together. Effectively, this makes '2 and '3 above the same | |
// as each other and distinct from '1. | |
trait MethodCaller<'a, ArgT, ResultT>: FnOnce(&'a Controller<TokioRuntime>, ArgT) -> Self::Fut { | |
type Fut: Future<Output = Result<ResultT, Box<dyn Error + Sync + Send>>>; | |
} | |
impl< | |
'a, | |
ArgT, | |
ResultT, | |
FnT: FnOnce(&'a Controller<TokioRuntime>, ArgT) -> Fut, | |
Fut: Future<Output = Result<ResultT, Box<dyn Error + Sync + Send>>>, | |
> MethodCaller<'a, ArgT, ResultT> for FnT | |
{ | |
type Fut = Fut; | |
} | |
/// This is a generic dispatcher that is used by the wrapper API to | |
/// call methods on the singleton. It takes a closure that takes a | |
/// &[Controller] and an arg, calls the closure using the singleton, | |
/// and returns the result. The [MethodCaller] trait ties the lifetime | |
/// of the controller to the lifetime of the Future. | |
fn run_method<ArgT, ResultT, FnT>( | |
f: FnT, | |
arg: ArgT, | |
) -> Result<ResultT, Box<dyn Error + Sync + Send>> | |
where | |
for<'a> FnT: MethodCaller<'a, ArgT, ResultT>, | |
// Some day, one of these will work: | |
// FnT: async FnOnce(&Controller, ArgT) -> Result<ResultT, Box<dyn Error + Sync + Send>>, | |
// FnT: std::ops::AsyncFnOnce(&Controller, ArgT) -> Result<ResultT, Box<dyn Error + Sync + Send>>, | |
{ | |
let lock = CONTROLLER.controller.read().unwrap(); | |
let Some(controller) = &*lock else { | |
return Err("call init first".into()); | |
}; | |
CONTROLLER.rt.block_on(f(controller, arg)) | |
} | |
pub fn init() { | |
let mut controller = CONTROLLER.controller.write().unwrap(); | |
*controller = Some(Controller::new()); | |
} | |
pub fn one(val: i32) -> Result<i32, Box<dyn Error + Sync + Send>> { | |
run_method(Controller::one, val) | |
} | |
pub fn two(val: &str) -> Result<String, Box<dyn Error + Sync + Send>> { | |
run_method(Controller::two, val) | |
} | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
#[test] | |
fn test_basic() { | |
// This is a duplication of the controller test using the | |
// wrapper API. | |
assert_eq!(two("quack").err().unwrap().to_string(), "call init first"); | |
init(); | |
assert_eq!(one(5).unwrap(), 1); | |
assert_eq!(one(3).err().unwrap().to_string(), "sorry, not that one"); | |
assert_eq!(two("potato").unwrap(), "two?val=potato&seq=2"); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use implbox::ImplBox; | |
use implbox_macros::implbox_decls; | |
use std::marker::PhantomData; | |
use std::ops::{Deref, DerefMut}; | |
pub trait Runtime: Default + Clone + Locker {} | |
/// The [AsyncRwLock::read] and [AsyncRwLock::write] functions must return | |
/// actual async-aware lock guards that maintain the lock until they are out of | |
/// scope. They must not block the thread while holding the lock. | |
pub trait AsyncRwLock<T> { | |
fn new(item: T) -> Self; | |
fn read( | |
&self, | |
) -> impl std::future::Future<Output = impl Deref<Target = T> + Sync + Send> + Send; | |
fn write( | |
&self, | |
) -> impl std::future::Future<Output = impl DerefMut<Target = T> + Sync + Send> + Send; | |
} | |
/// This is an empty structure that we use as the generic type for ImplBox. | |
pub struct LockBox<T>(PhantomData<T>); | |
/// This trait glues ImplBox to AsyncRwLock and enables creation of AsyncRwLocks | |
/// of any type. | |
pub trait Locker { | |
#[implbox_decls(LockBox<T>)] | |
fn new_lock<T: Sync + Send>(item: T) -> impl AsyncRwLock<T>; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::rwlock::TokioLockWrapper; | |
use base::{AsyncRwLock, LockBox, Locker, Runtime}; | |
use implbox::ImplBox; | |
use implbox_macros::implbox_impls; | |
pub mod rwlock; | |
#[derive(Default, Clone)] | |
pub struct TokioRuntime; | |
impl Locker for TokioRuntime { | |
#[implbox_impls(LockBox<T>, TokioLockWrapper<T>)] | |
fn new_lock<T: Sync + Send>(item: T) -> impl AsyncRwLock<T> { | |
TokioLockWrapper::<T>::new(item) | |
} | |
} | |
impl Runtime for TokioRuntime {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use base::AsyncRwLock; | |
use std::ops::{Deref, DerefMut}; | |
use tokio::sync; | |
#[derive(Default)] | |
pub struct TokioLockWrapper<T> { | |
lock: sync::RwLock<T>, | |
} | |
impl<T: Sync + Send> AsyncRwLock<T> for TokioLockWrapper<T> { | |
fn new(item: T) -> Self { | |
TokioLockWrapper { | |
lock: sync::RwLock::new(item), | |
} | |
} | |
async fn read(&self) -> impl Deref<Target = T> + Sync + Send { | |
self.lock.read().await | |
} | |
async fn write(&self) -> impl DerefMut<Target = T> + Sync + Send { | |
self.lock.write().await | |
} | |
} | |
#[cfg(test)] | |
mod tests; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use super::*; | |
use crate::TokioRuntime; | |
use base::{LockBox, Locker}; | |
use implbox::ImplBox; | |
use std::marker::PhantomData; | |
use std::sync::Arc; | |
use std::time::Duration; | |
use tokio::sync::oneshot; | |
use tokio::task; | |
struct Thing<LockerT: Locker> { | |
lock: ImplBox<LockBox<i32>>, | |
_l: PhantomData<LockerT>, | |
} | |
impl<LockerT: Locker> Thing<LockerT> { | |
fn new(item: i32) -> Self { | |
Self { | |
lock: LockerT::box_lock(item), | |
_l: Default::default(), | |
} | |
} | |
fn lock(&self) -> &(impl AsyncRwLock<i32> + '_) { | |
LockerT::unbox_lock(&self.lock) | |
} | |
async fn do_thing(&self) -> i32 { | |
let mut m = self.lock().write().await; | |
async move { std::ptr::null::<*const ()>() }.await; | |
*m += 1; | |
*m | |
} | |
} | |
async fn generic_thing<M>(m: &M) | |
where | |
M: AsyncRwLock<i32>, | |
{ | |
{ | |
// Hold lock across an await point. We don't get warnings for this, and | |
// as long as RwLock is implemented using an async-aware RwLock, we're | |
// fine. | |
let lock = m.read().await; | |
// non-Send Future | |
async move { std::ptr::null::<*const ()>() }.await; | |
assert_eq!(*lock, 3); | |
} | |
{ | |
let mut lock = m.write().await; | |
// non-Send Future | |
async move { std::ptr::null::<*const ()>() }.await; | |
*lock = 4; | |
} | |
{ | |
let lock = m.read().await; | |
assert_eq!(*lock, 4); | |
async move {}.await; | |
} | |
} | |
#[tokio::test(flavor = "current_thread")] | |
async fn test_basic() { | |
let l1 = Arc::new(TokioRuntime::box_lock(3)); | |
let m1 = TokioRuntime::unbox_lock(l1.as_ref()); | |
generic_thing(m1).await; | |
let l2 = l1.clone(); | |
assert_eq!(*m1.read().await, 4); | |
let h = task::spawn(async move { | |
let m2 = TokioRuntime::unbox_lock(l2.as_ref()); | |
let mut lock = m2.write().await; | |
// non-Send Future | |
async move { std::ptr::null::<*const ()>() }.await; | |
*lock = 5; | |
1 | |
}); | |
assert_eq!(1, h.await.unwrap()); | |
let lock = m1.read().await; | |
assert_eq!(*lock, 5); | |
} | |
#[tokio::test(flavor = "current_thread")] | |
async fn test_lock() { | |
// Exercise non-trivial case of waiting for a lock. | |
let m1 = Arc::new(TokioRuntime::new_lock(5)); | |
let (tx, rx) = oneshot::channel::<()>(); | |
let m2 = m1.clone(); | |
let h1 = task::spawn(async move { | |
// Grab the lock first, then signal to the other task. | |
let mut lock = m2.write().await; | |
tx.send(()).unwrap(); | |
// We got the lock first. The other side can't progress. | |
tokio::time::sleep(Duration::from_millis(10)).await; | |
assert_eq!(*lock, 5); | |
*lock = 10; | |
// When we finish, we automatically release the lock. | |
}); | |
let m2 = m1.clone(); | |
let h2 = task::spawn(async move { | |
// Wait for the first the channel, and then grab the lock. | |
rx.await.unwrap(); | |
// Try to get the lock. This will "block" (yield to the runtime) until | |
// the lock is available. | |
let mut lock = m2.write().await; | |
// The other side has finished. | |
assert_eq!(*lock, 10); | |
*lock = 11; | |
}); | |
// Wait for the jobs to finish. | |
h1.await.unwrap(); | |
h2.await.unwrap(); | |
let lock = m1.read().await; | |
assert_eq!(*lock, 11); | |
} | |
#[tokio::test(flavor = "current_thread")] | |
async fn test_locker() { | |
let th = Thing::<TokioRuntime>::new(3); | |
let m = TokioRuntime::unbox_lock(&th.lock); | |
generic_thing(m).await; | |
assert_eq!(th.do_thing().await, 5); | |
async {}.await; | |
assert_eq!(th.do_thing().await, 6); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! ImplBox provides a workaround for the lack of ability to assign an | |
//! opaque type (`impl SomeTrait`) to a struct field or use it in many | |
//! other places. As of rust 1.83, an impl type may appear in a | |
//! function parameter or return type, but you can't use it in any | |
//! other place, including closures, struct fields, or simple variable | |
//! declarations. There are various proposals for language changes | |
//! that would make that possible. See | |
//! <https://github.com/rust-lang/rust/issues/63063>. | |
//! | |
//! ImplBox works by storing the impl type as an untyped raw pointer | |
//! and providing a mechanism to delegate conversion of the raw | |
//! pointer back to a reference to a concrete implication, which can | |
//! return it as a reference to the impl type. In this way, it acts as | |
//! a proxy so that the ImplBox can be stored where you would want to | |
//! store the impl type reference. ImplBox uses unsafe code, but as | |
//! long as it is used properly, it is safe. To assist, ImplBox | |
//! provides some macros to generate correct code. | |
//! | |
//! # Typical Usage | |
//! | |
//! See the example for a concrete explanation with comments. | |
//! | |
//! Use ImplBox if you have a trait that has an associated function | |
//! that returns an impl type and you want to store the result. Let's | |
//! call the trait `Thing`. To use [ImplBox] as a proxy for `Thing`: | |
//! - Create a new trait with an associated function that returns | |
//! `impl Thing`, probably by proxying to an associated function in | |
//! `Thing`. Let's call it `ThingMaker`. | |
//! - In the Trait's declaration, declare a method whose name starts | |
//! with `new_` and that returns an opaque type, e.g. `new_thing() | |
//! -> impl Thing`. It can take any additional arguments that may be | |
//! required. | |
//! - Annotate the declaration with `#[implbox_decl]`. If your | |
//! function is called `new_thing`, this will create `box_thing`, | |
//! `unbox_thing`, and `drop_thing`. | |
//! - In the implementation of `ThingMaker` for some concrete type, | |
//! annotate the implementation of `new_thing` with | |
//! `#[implbox_impls]`. | |
//! - In code that needs to use `&impl Thing`: | |
//! - Call `new_thing_implbox` instead of `new_thing`. This returns | |
//! an `ImplBox`, which can be stored anywhere. | |
//! - To get the `&impl Thing`, call the associated | |
//! `unbox_new_thing` method with a reference to the `ImplBox`. | |
//! This returns a reference to the thing. It is useful to create | |
//! a separate method that does this. | |
//! - You never call `drop_thing` -- it is called automatically when | |
//! the `ImplBox` is dropped. | |
//! | |
//! The [ImplBox] type has a generic type parameter. There is no | |
//! specifically defined relationship between that type and the type | |
//! the [ImplBox] is proxying. The type can never be the exact type | |
//! since `impl SomeTrait` is not a concrete type. The generic type | |
//! for [ImplBox] is used in the following ways: | |
//! - A specific [ImplBox] will be `Sync` and `Send` if and only if | |
//! the generic type is `Sync` and `Send` | |
//! - Using a unique type for each thing you are storing in an | |
//! [ImplBox] enables you to get a compile error if you try to pass | |
//! an [ImplBox] to the wrong unbox method. There is also a runtime | |
//! check for this in case you get it wrong. Therefore, a good | |
//! strategy is to create a unique "shadow type" with the same | |
//! generics, if any, as the type you are boxing. That way, there | |
//! will be an exact, one-to-one correspondence between a concrete | |
//! instantiation of `ImplBox` and the type it contains. This | |
//! ensures that you will get a compile error if you try to convert | |
//! an `ImplBox` to the wrong type, and along with the macros, | |
//! guarantees safety of `ImplBox`. | |
//! | |
//! # Safety | |
//! - You must only convert an ImplBox's pointer back to the concrete | |
//! type that it originally came from. This can only be done by the | |
//! concrete type. | |
//! - You must not do anything with the pointer that wouldn't be | |
//! allowed by borrowing rules, such as returning a mutable | |
//! reference from an immutable ImplBox. | |
//! - You must use a generic type with [ImplBox] whose `Sync`/`Send` | |
//! status is the same as for the concrete trait implementation that | |
//! you are storing. | |
//! - Using the macros and shadow type as described above guarantees | |
//! that all these constraints are satisfied. To prevent | |
//! accidentally passing an `ImplBox` to the wrong concrete | |
//! implementation of the trait, runtime checks using `TypeId` | |
//! supplement these compile-time checks and would be sufficient if | |
//! the compile-time helper types were used incorrectly. | |
//! | |
//! # Example | |
//! ``` | |
//! use implbox::ImplBox; | |
//! use implbox_macros::{implbox_decls, implbox_impls}; | |
//! use std::marker::PhantomData; | |
//! | |
//! // This generic trait has an associated function that returns an | |
//! // impl type. A concrete Food type would implement this trait. | |
//! trait Food<T: Clone> { | |
//! fn new(prep: T) -> impl Food<T>; | |
//! fn prep(&self) -> T; | |
//! } | |
//! | |
//! // Here's a concrete food implementation. | |
//! struct Potato<T> { | |
//! prep: T, | |
//! } | |
//! impl<T: Clone> Food<T> for Potato<T> { | |
//! fn new(prep: T) -> impl Food<T> { | |
//! Self { prep } | |
//! } | |
//! | |
//! fn prep(&self) -> T { | |
//! self.prep.clone() | |
//! } | |
//! } | |
//! | |
//! // We can't store `&impl Food<T>` in a struct field, so enhance | |
//! // `Food` by gluing it to `ImplBox`. | |
//! | |
//! // Create a dummy type that to provide extra compile-time | |
//! // checking. By using `FoodBox<T>` as the generic type for any | |
//! // `ImplBox` that actually holds some `Food<T>`, we make it a | |
//! // compile error if we try to get a concrete `Food` out of the | |
//! // wrong type of `ImplBox`. Runtime checks using `TypeId` ensure | |
//! // that we actually have the right concrete type. | |
//! struct FoodBox<T>(PhantomData<T>); | |
//! | |
//! // This trait provides the glue between the original trait and the | |
//! // ImplBox. For each concrete implementation of the original | |
//! // trait, create a corresponding concrete implementation for the | |
//! // helper that creates the corresponding concrete implementation | |
//! // of the trait. Note the use of the implbox macros in declaring | |
//! // and defining the methods. All we have to supply is some proxy | |
//! // around the `Food` constructor. It has to be called | |
//! // `new_something`. Note that `FoodHelper` is not a generic type. | |
//! // Since the generic parameter is attached to `new_food`, a type | |
//! // that implements `FoodHelper` can create a `Food` of any type. | |
//! // The generic type in `FoodBox<T>` comes from the <T> of | |
//! // `new_food`. The argument to `implbox_decls` is the generic type | |
//! // of `ImplBox`. If it has any of its own generics, they are taken | |
//! // from the generics of the `new` function that is being | |
//! // annotated. | |
//! trait FoodHelper { | |
//! #[implbox_decls(FoodBox<T>)] | |
//! fn new_food<T: Clone>(prep: T) -> impl Food<T>; | |
//! } | |
//! | |
//! // We need a concrete `FoodHelper` for each concrete `Food` | |
//! // implementation. The arguments to `implbox_impls` are the | |
//! // generic type for the `ImplBox` and the concrete type that is | |
//! // being stored. | |
//! struct PotatoHelper; | |
//! impl FoodHelper for PotatoHelper { | |
//! #[implbox_impls(FoodBox<T>, Potato<T>)] | |
//! fn new_food<T: Clone>(prep: T) -> impl Food<T> { | |
//! Potato::new(prep) | |
//! } | |
//! } | |
//! | |
//! // Here's a struct that holds an impl type of Food. We can't make | |
//! // the field `food` have type `&impl Food<String>`, so we make it | |
//! // have type `ImplBox<FoodBox<String>>` instead. See how we use | |
//! // `FoodBox`. The `ImplBox` can't be `ImplBox<impl Food<String>>` | |
//! // because impl types are not valid in that position, and it can't | |
//! // be the concrete type because we don't know what the concrete | |
//! // type is in the trait declaration. We can't just make `FoodT` a | |
//! // generic type and store a `FoodT` directly because want | |
//! // `FoodHelper` to have to know at compile time what types of | |
//! // items it will create. | |
//! struct Refrigerator<FoodHelperT: FoodHelper> { | |
//! food: ImplBox<FoodBox<String>>, | |
//! _f: PhantomData<FoodHelperT>, | |
//! } | |
//! // Add a convenience method to get the food out. | |
//! impl<FoodHelperT: FoodHelper> Refrigerator<FoodHelperT> { | |
//! fn food(&self) -> &(impl Food<String> + '_) { | |
//! FoodHelperT::unbox_food(&self.food) | |
//! } | |
//! } | |
//! | |
//! // This shows how to use it. Instead of storing the return value | |
//! // of `new_food` in a field of type `impl Food`, we store the | |
//! // return value of `box_food` in a field of type `ImplBox`. Then | |
//! // we ask the concrete type to get the impl back out. | |
//! let r = Refrigerator::<PotatoHelper> { | |
//! food: PotatoHelper::box_food("baked".to_string()), | |
//! _f: Default::default(), | |
//! }; | |
//! // If `food` where `impl Food`, we could just call | |
//! // `r.food.prep()`. Instead, we call `r.food().prep()` to | |
//! // indirect through the ImplBox. | |
//! assert_eq!(r.food().prep(), "baked"); | |
//! ``` | |
use std::any::TypeId; | |
use std::marker::PhantomData; | |
unsafe impl<T: Send> Send for ImplBox<T> {} | |
unsafe impl<T: Sync> Sync for ImplBox<T> {} | |
pub struct ImplBox<T> { | |
id: TypeId, | |
ptr: *const (), | |
destroy: fn(*const ()), | |
_t: PhantomData<T>, | |
} | |
impl<T> ImplBox<T> { | |
pub fn new(id: TypeId, destroy: fn(*const ()), ptr: *const ()) -> Self { | |
Self { | |
id, | |
ptr, | |
destroy, | |
_t: Default::default(), | |
} | |
} | |
pub fn with<F, Ret>(&self, id: TypeId, f: F) -> Ret | |
where | |
F: FnOnce(*const ()) -> Ret, | |
{ | |
if self.id == id { | |
f(self.ptr) | |
} else { | |
panic!("id mismatch"); | |
} | |
} | |
} | |
impl<T> Drop for ImplBox<T> { | |
fn drop(&mut self) { | |
(self.destroy)(self.ptr); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate proc_macro; | |
use proc_macro::TokenStream; | |
use quote::{format_ident, quote, ToTokens}; | |
use syn::parse::{Parse, ParseStream, Parser}; | |
use syn::punctuated::Punctuated; | |
use syn::token::Comma; | |
use syn::{parse, parse_macro_input, FnArg, ImplItemFn, ReturnType, TraitItemFn, Type, TypePath}; | |
struct DeclAttrs { | |
generic: TypePath, | |
} | |
impl Parse for DeclAttrs { | |
fn parse(input: ParseStream) -> parse::Result<Self> { | |
Ok(DeclAttrs { | |
generic: input.parse()?, | |
}) | |
} | |
} | |
type ImplAttrs = Punctuated<TypePath, Comma>; | |
#[proc_macro_attribute] | |
pub fn implbox_decls(args: TokenStream, input: TokenStream) -> TokenStream { | |
let item_decl = parse_macro_input!(input as TraitItemFn); | |
let attr = parse_macro_input!(args as DeclAttrs); | |
let generic_type = attr.generic; | |
let orig = item_decl.clone(); | |
let sig = item_decl.sig; | |
let generics = sig.generics; | |
let ident = sig.ident; | |
let asyncness = sig.asyncness; | |
let constness = sig.constness; | |
let inputs = sig.inputs; | |
let output = sig.output; | |
let unsafety = sig.unsafety; | |
let output = create_box_output(output); | |
let ident_str = ident.to_string(); | |
let Some(base) = ident_str.strip_prefix("new_") else { | |
panic!("function for implbox_decls must be new_something"); | |
}; | |
let box_fn = format_ident!("box_{}", base); | |
let unbox_fn = format_ident!("unbox_{}", base); | |
let drop_fn = format_ident!("drop_{}", base); | |
// `pub`, `default`, `const`, `async`, `unsafe`, `extern` | |
let gen = quote! { | |
#orig | |
/// Generated by implbox_decls -- call to create the boxed value | |
#asyncness #constness #unsafety fn #box_fn #generics (#inputs) -> ImplBox<#generic_type>; | |
/// Generated by implbox_decls -- call to retrieve original value | |
fn #unbox_fn #generics(l: &ImplBox<#generic_type>) #output; | |
/// Generated by implbox_decls -- called automatically | |
fn #drop_fn #generics (p: *const ()); | |
}; | |
gen.into() | |
} | |
#[proc_macro_attribute] | |
pub fn implbox_impls(args: TokenStream, input: TokenStream) -> TokenStream { | |
let item_impl = parse_macro_input!(input as ImplItemFn); | |
let attr = ImplAttrs::parse_terminated.parse(args).unwrap(); | |
let mut iter = attr.iter(); | |
let generic_type = iter.next().unwrap(); | |
let concrete_path = iter.next().unwrap(); | |
if iter.next().is_some() { | |
panic!("too many parameters to implbox_impls"); | |
} | |
let orig = item_impl.clone(); | |
let sig = item_impl.sig; | |
let generics = sig.generics; | |
let ident = sig.ident; | |
let asyncness = sig.asyncness; | |
let constness = sig.constness; | |
let inputs = sig.inputs; | |
let output = sig.output; | |
let unsafety = sig.unsafety; | |
let output = create_box_output(output); | |
let (_g_impl, g_type, _g_where) = generics.split_for_impl(); | |
let g_fish = g_type.as_turbofish(); | |
let ident_str = ident.to_string(); | |
let Some(base) = ident_str.strip_prefix("new_") else { | |
panic!("function for implbox_decls must be new_something"); | |
}; | |
let box_fn = format_ident!("box_{}", base); | |
let unbox_fn = format_ident!("unbox_{}", base); | |
let drop_fn = format_ident!("drop_{}", base); | |
let mut params = Vec::new(); | |
for arg in inputs.iter() { | |
match arg { | |
FnArg::Receiver(r) => params.push(r.to_token_stream()), | |
FnArg::Typed(t) => params.push(t.pat.to_token_stream()), | |
} | |
params.push(quote! {}); | |
} | |
// `pub`, `default`, `const`, `async`, `unsafe`, `extern` | |
let gen = quote! { | |
#orig | |
#asyncness #constness #unsafety fn #box_fn #generics (#inputs) -> ImplBox<#generic_type> { | |
let item = Self::#ident(#(#params)*); | |
let ptr = Box::into_raw(Box::new(item)); | |
ImplBox::new(std::any::TypeId::of::<Self>(), Self::#drop_fn #g_fish, ptr as *const ()) | |
} | |
fn #unbox_fn #generics (l: &ImplBox<#generic_type>) #output { | |
l.with(std::any::TypeId::of::<Self>(), |p| { | |
let p = p as *const #concrete_path; | |
unsafe { p.as_ref() }.unwrap() | |
}) | |
} | |
fn #drop_fn #generics (p: *const ()) { | |
drop(unsafe { Box::from_raw(p as *mut #concrete_path) }); | |
} | |
}; | |
gen.into() | |
} | |
fn create_box_output(orig: ReturnType) -> ReturnType { | |
match orig { | |
ReturnType::Default => ReturnType::Default, | |
ReturnType::Type(arr, t) => { | |
let t = *t; | |
let tokens = t.to_token_stream(); | |
let tokens_str = tokens.to_string(); | |
if !tokens_str.starts_with("impl ") { | |
panic!("original return type must start with impl"); | |
} | |
let t = quote! { &#tokens }; | |
let t: Type = syn::parse2(t).unwrap(); | |
ReturnType::Type(arr, Box::new(t)) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment