Skip to content

Instantly share code, notes, and snippets.

@jberkenbilt
Created December 2, 2024 00:50
Show Gist options
  • Save jberkenbilt/b6350fef1a974ef613a3091147ef587d to your computer and use it in GitHub Desktop.
Save jberkenbilt/b6350fef1a974ef613a3091147ef587d to your computer and use it in GitHub Desktop.
Go to Rust: final code
//! 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 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");
}
}
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>;
}
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 {}
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;
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);
}
//! 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);
}
}
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