#proc-macro #enums #traits #helper #enumizer

no-std trait-enumizer

Proc macro to automatically generate enum based on method signatures (with appropriate helpers)

2 releases

0.1.1 Apr 17, 2022
0.1.0 Mar 16, 2022

#1420 in Rust patterns

MIT/Apache

40KB
224 lines

trait-enumizer

img

Generate enum from a trait, with converters between them.

Features

  • Generation of enum based on specified trait (or inherent impl) where each variant corresponds to a method. Arguments correpond to fields of the variants; return values, correspond to a sender of some channel.
  • Generation call functions function with a appropriate match generated inside. This allows the enum to be "applied" to an object implementing the trait.
  • Generation of a proxy that allows obtaining a sequence of enum values using method calls from original trait (if possible) or API similar to original. Proxy also helps dealing with "channelizing" return values.
  • Handling return values.
  • Handling async (inherent impl only).
  • I tried to make the crate no_std-friendly, but have not tested it.

The library can be used as a synchronisation mechanism or as a building block to build actors or remote procedure calls.

Main piece of library is enumizer attribute macro. It should be attached to trait or inherent impl. It copies input trait or impl to proc macro output (sans pseudo-derive-helpers attributes), then also generates the following items:

  • Enum where each variant represent a method
  • Zero or more "call functions"
  • Zero or more "proxy structs"

Parameters

#[trait_enumizer::enumizer(...)] accepts following parameters:

  • name=<ident> - Set the enum name. Required.
  • pub, pub_crate - Mark all generated items as pub or pub(crate) respectively
  • returnval=<macro_class_name> - Enable more complex mode where return values are handled. Affects API of other generated items (call_fns and proxies) as well. Input macros is used as a makeshift GAT trait with specialisation. See a dedicated README section for more info.
  • enum_attr - Inject custom attribute (e.g. enum_attr[derive(serde_derive::Serialize)]) into enum declaration. Can be repeated. You need to use square brackets for this.
  • inherent_impl - Base enum on an inherent impl instead of a trait.
  • call_fn() - See below.
  • proxy() - See below.

Example of attributes syntax:

#[trait_enumizer::enumizer(pub_crate, name=NewEnumName, call_fn(name=call_me,ref_mut,allow_panic), proxy(name=NewEnumNameProxy, Fn,unwrapping_impl))]

Call functions (call_fns)

Call functions are generated when you use call_fn() parameter. They use the following subparameters:

  • name=<ident> - name of the inherent method. Required.
  • ref or ref_mut (alias mut_ref) or once (alias move) - Accept the object by reference, by mutable reference or by value. Required.
  • allow_panic - Allow generation of the function with panic!() calls inside.
  • async - Generate async fn. Use send_async pseudomethod from returnval macro-class instead of send.
  • extra_arg_type(<type>) - Add additional argument to the try_call function. That argument will appear on all macro_class_name!(send(...)) callbacks.

Those functions are used to "convert" enum value into a method call. Call functions are generated as inherent impl functions of the generated enum. First argument is self. Second argument is the value of (or reference to) something implementing the trait you specified (skipping the trait in inherent_impl mode). Third argument is required if you specify extra_arg_type(). It is passed to returnval's send (or send_async) pseudomethod for customized handling of return values.

Example:

#[enumizer(name=QqqEnum,pub,call_fn(name=the_call,ref_mut))]
trait Qqq {
    ...
}

generates

enum QqqEnum { ... }
impl QqqEnum {
    pub fn the_call<I: Qqq>(self, o: &mut I);
}

Proxies

Proxies are generated when you use proxy() parameter. They use the following subparameters:

  • Fn, FnMut, FnOnce - Set type of closure that the proxy will carry. Required.
  • name=<ident> - name of the generated struct. Required.
  • resultified_trait=<ident> - Also generate "resultified" trait instead of implementing try_* functions inherently.
  • infallible_impl - Make the proxy also implement the original trait, provided your sink function does not err.
  • unwrapping_impl - Make the proxy also implement the original trait, using unwrap where needed.
  • unwrapping_and_panicking_impl - Force proxy to implement the original trait, using panic!() calls where complication would fail because of ownership requirements.
  • extra_field_type(...) - Add additional second field to proxy struct. That field will be used as additional argument to macro_class_name!(create(...)) and macro_class_name!(recv(...)) callbacks.
  • async - Expect user-specified closure to return Future<Output=Result> instead of just Result and use .awaits inside where appropriate.

A proxy is a generic tuple struct with a public field. That field should implement Fn, FnMut or FnOnce. Second field (also public) is created if you specify extra_field_type(). There are two generic parameters: error type (you choose it) and closure type. Proxies allow "converting" method calls to enum values (which get delivered to your closure). By default all input methods are renamed, having "try_" prepended. Typically they return Result<(), YourErrorType>, but in returnval mode some of them may return Result<Result<T, SendError>, YourErrorType>. There is async mode, which upgrades your function to return Future and makes all the try_* methods async. You can ask Enumizer to also generate "resultified" trait which proxy then implements (unless async, of course). async also affects returnval macro usage.

You can also ask Enumizer to make proxy implement the original trait (also unless async). There are two strategies for it: infallible (if return values are not used and your Fn opts out of error handling by using std::convert::Infallible) and unwrapping.

You can make async proxy for non-async original methods and vice versa.

Example (simplified):

#[enumizer(name=QqqEnum,proxy(FnMut,name=QqqProxy))]
trait Qqq {
    fn foo(&self,x : i32);
}

generates

enum QqqEnum { Foo{x:i32} }
struct QqqProxy<E,F>(pub F)
    where F: FnMut(QqqEnum) -> Result<(), E>;
impl<E,F> QqqProxy where ... {
    fn try_foo(&mut self, x: i32) -> Result<(), E>{
        (self.0)(QqqEnum::Foo{x})
    }
}

Pseudo-derive-helpers

Pseudo-derive-helpers are attributes that are handled by this library. You are supposed to used them inside your input trait or impl before method signature or before argument inside signature. Other (unknown) attributes are passed though unmodified.

  • #[enumizer_enum_attr[...]] - Forward specified attribute to generated enum. Example: #[enumizer_enum_attr[serde(rename="qqq")]]. Can be attached to functions (which become enum variants) or to function arguments (which become enum variant fields).
  • #[enumizer_return_attr[...]] - in returnval mode, attach custom attribute to the ret field of the enum.
  • #[enumizer_to_owned] - For reference argument type, use owned value instead of trying to put reference to enum (which may not work, unless 'static).

Returnval pseudotrait

If you want Enumizer to handle return values, you need a channel of some sort. Enumizer is flexible in channel choice. There are built in "classes" for some popular channel types, you may also need to implement a channel class yourself.

You specify channel class as value for returnval parameter, e.g. returnval=trait_enumizer::flume_class. Early in Enumizer design channel classes were traits using GAT, but now they are special macro_rules-based macros.

Here is API of a channel class:

macro_rules! my_channelclass {
    (Sender<$T:ty>) => { /* type of the `ret` field in enum variant */ };
    (SendError) => { /* Error type when failed to send to a channel. Must not depend on T */ };
    (RecvError) => { /* Error type when failed to receive from a channel */ };
    (create::<$T:ty>()) => {
         /* Expression returning (tx, rx) channel pair. `tx` must be of type `Sender`. */
         /* Used by proxies */
    };
    (send::<$T:ty>($channel:expr, $msg:expr /*, $extraarg:expr */)) => { 
         /* Expression used to send to the channel (for `call_fn`). You may need to map error type here */
    };
    (recv::<$T:ty>($channel:expr /*, $extrafield:expr */)) => { 
        /* Expression use to recv value from the channel (for proxy) */
     };
    (send_async::<$T:ty>($channel:expr, $msg:expr)) => { 
        /* Expression to send to channel from async `call_fn`s. Should include `.await` and error mapping */
    };
    (recv_async::<$T:ty>($channel:expr)) => { 
        /* Expression to receive from cahnnel in async proxies. Should include `.await`. */
     };
}

You are recommended to base your implementation on one of the built-in channel class (e.g. flume_class) or to use RPC sample as a template for trickier channel class.

Although returnval mechanism use "channel" terminology, Senders are not required to be actual channels. They may be some internal IDs, with the real channel being supplied as an additional argument.

When Enumizer encountres a method with return value, corresponding enum variant gains additional field named ret (a hard coded identifier). Type of this field is controlled by the channel class and may depend on the type of the return value. All interactions with this additional field go though channel class's pseudomethods.

Tests (also serve as documentation)

To understand how the crate functions, you can view some test files. For most samples there is corresponding "manual" sample, showing expanded version (sometimes slightly simplified) of the same test.

Note that those links may be broken on Docs.rs, use README on Github instead.

Cargo features

Most features enable respective channel classes. std crate feature, which is enabled by default, apart from enabling stdmpsc channel class also enables enumizer_to_owned through ::std::borrow::ToOwned trait. alloc feature enables alternative enumizer_to_owned mode using ::alloc::borrow::ToOwned instead (but you need to declare extern crate alloc; yourself). std supersedes alloc.

See also

Dependencies

~1.4–7MB
~62K SLoC