RPC: Initial implementation of a multiple-argument dispatch
This code uses some kludges (discussed with Ian previously and hopefully well documented here) to get a type-identifier for each type in a const context. It then defines a macro to declare a type-erased versions of a concrete implementation functions, and register those implementations to be called later. We will probably want to tweak a bunch of this code as we move ahead.
This commit is contained in:
parent
a4660a4e09
commit
e26d9452dc
|
@ -270,6 +270,12 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert-impl"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3464313de0c867016e3e69d7e1e9ae3499bcc4c18e12283d381359ed38b5b9e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -4379,8 +4385,13 @@ dependencies = [
|
||||||
name = "tor-rpccmd"
|
name = "tor-rpccmd"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"assert-impl",
|
||||||
|
"downcast-rs",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-await-test",
|
"futures-await-test",
|
||||||
|
"inventory",
|
||||||
|
"once_cell",
|
||||||
|
"paste",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"typetag",
|
"typetag",
|
||||||
|
|
|
@ -148,6 +148,7 @@ mod test {
|
||||||
|
|
||||||
#[typetag::deserialize(name = "dummy")]
|
#[typetag::deserialize(name = "dummy")]
|
||||||
impl rpc::Command for DummyCmd {}
|
impl rpc::Command for DummyCmd {}
|
||||||
|
tor_rpccmd::impl_const_type_id!(DummyCmd); // XXXX
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct DummyResponse {
|
struct DummyResponse {
|
||||||
|
|
|
@ -12,12 +12,16 @@ categories = ["asynchronous"]
|
||||||
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
|
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
#XXXX remove whatever is unneeded here.
|
downcast-rs = "1.2.0"
|
||||||
futures = "0.3.14"
|
futures = "0.3.14"
|
||||||
serde = { version = "1.0.103", features = [ "derive"] }
|
futures-await-test = "0.3.0"
|
||||||
|
inventory = "0.3.5"
|
||||||
|
once_cell = "1"
|
||||||
|
paste = "1"
|
||||||
|
serde = { version = "1.0.103", features = ["derive"] }
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
typetag = "0.2.7"
|
typetag = "0.2.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
#XXXX remove whatever is unneeded here.
|
assert-impl = "0.1.3"
|
||||||
futures-await-test = "0.3.0"
|
futures-await-test = "0.3.0"
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
use downcast_rs::Downcast;
|
||||||
|
|
||||||
|
use crate::typeid::GetConstTypeId_;
|
||||||
|
|
||||||
/// The parameters and method name associated with a given Request.
|
/// The parameters and method name associated with a given Request.
|
||||||
///
|
///
|
||||||
/// We use [`typetag`] here so that we define `Command`s in other crates.
|
/// We use [`typetag`] here so that we define `Command`s in other crates.
|
||||||
|
@ -10,7 +14,6 @@
|
||||||
// TODO RPC: Possible issue here is that, if this trait is public, anybody outside
|
// TODO RPC: Possible issue here is that, if this trait is public, anybody outside
|
||||||
// of Arti can use this trait to add new commands to the RPC engine. Should we
|
// of Arti can use this trait to add new commands to the RPC engine. Should we
|
||||||
// care?
|
// care?
|
||||||
#[typetag::deserialize(tag = "method", content = "data")]
|
#[typetag::deserialize(tag = "method", content = "params")]
|
||||||
pub trait Command: std::fmt::Debug + Send {
|
pub trait Command: GetConstTypeId_ + std::fmt::Debug + Send + Downcast {}
|
||||||
// TODO RPC: this will need some kind of "run this command" trait.
|
downcast_rs::impl_downcast!(Command);
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,228 @@
|
||||||
|
//! A multiple-argument dispatch system for our RPC system.
|
||||||
|
//!
|
||||||
|
//! Our RPC functionality is polymorphic in Commands (what we're told to do) and
|
||||||
|
//! Objects (the things that we give the commands to); we want to be able to
|
||||||
|
//! provide different implementations for each command, on each object.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use futures::future::BoxFuture;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::typeid::ConstTypeId_;
|
||||||
|
use crate::{Command, Context, Object};
|
||||||
|
|
||||||
|
/// The return type from an RPC function.
|
||||||
|
type RpcResult = String; // XXXX Not the actual type we want.
|
||||||
|
|
||||||
|
// A boxed future holding the result of an RPC command.
|
||||||
|
type RpcResultFuture = BoxFuture<'static, RpcResult>;
|
||||||
|
|
||||||
|
/// A type-erased RPC-command invocation function.
|
||||||
|
///
|
||||||
|
/// This function takes `Arc`s rather than a reference, so that it can return a
|
||||||
|
/// `'static` future.
|
||||||
|
type ErasedInvokeFn = fn(Arc<dyn Object>, Box<dyn Command>, Arc<dyn Context>) -> RpcResultFuture;
|
||||||
|
|
||||||
|
/// An entry for our dynamic dispatch code.
|
||||||
|
///
|
||||||
|
/// These are generated using [`inventory`] by our `rpc_invoke_fn` macro;
|
||||||
|
/// they are later collected into a more efficient data structure.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct InvokeEntry_ {
|
||||||
|
obj_id: ConstTypeId_,
|
||||||
|
cmd_id: ConstTypeId_,
|
||||||
|
func: ErasedInvokeFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that using `inventory` here means that _anybody_ can define new
|
||||||
|
// commands! This may not be the greatest property.
|
||||||
|
inventory::collect!(InvokeEntry_);
|
||||||
|
|
||||||
|
impl InvokeEntry_ {
|
||||||
|
/// Create a new `InvokeEntry_`.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub const fn new(obj_id: ConstTypeId_, cmd_id: ConstTypeId_, func: ErasedInvokeFn) -> Self {
|
||||||
|
InvokeEntry_ {
|
||||||
|
obj_id,
|
||||||
|
cmd_id,
|
||||||
|
func,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Declare an RPC function that will be used to call a single type of [`Command`] on a
|
||||||
|
/// single type of [`Object`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tor_rpccmd as rpc;
|
||||||
|
///
|
||||||
|
/// #[derive(Debug)]
|
||||||
|
/// struct ExampleObject {}
|
||||||
|
/// impl rpc::Object for ExampleObject {}
|
||||||
|
///
|
||||||
|
/// #[derive(Debug,serde::Deserialize)]
|
||||||
|
/// struct ExampleCommand {}
|
||||||
|
/// #[typetag::deserialize]
|
||||||
|
/// impl rpc::Command for ExampleCommand {}
|
||||||
|
///
|
||||||
|
/// // TODO RPC Hide this macro.
|
||||||
|
/// rpc::impl_const_type_id!{ExampleObject ExampleCommand}
|
||||||
|
///
|
||||||
|
/// rpc::rpc_invoke_fn!{
|
||||||
|
/// // XXXX wrong return type.
|
||||||
|
/// async fn example(obj: ExampleObject, cmd: ExampleCommand, ctx) -> String {
|
||||||
|
/// // XXXX In this function, obj is actually an Arc<ExampleObject>,
|
||||||
|
/// // cmd is actually a Box<ExampleCommand>,
|
||||||
|
/// // and ctx is actually an Arc<dyn rpc::Context>!
|
||||||
|
/// //
|
||||||
|
/// // XXXX This is quite infelicitous, since the details are hidden. We should
|
||||||
|
/// // probably make the type information explicit instead.
|
||||||
|
/// println!("Running example command!");
|
||||||
|
/// "here is your result".into()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! rpc_invoke_fn {
|
||||||
|
{
|
||||||
|
$(#[$meta:meta])*
|
||||||
|
$v:vis async fn $name:ident($obj:ident : $objtype:ty, $cmd:ident: $cmdtype:ty, $ctx:ident) -> $rtype:ty {
|
||||||
|
$($body:tt)*
|
||||||
|
}
|
||||||
|
$( $($more:tt)+ )?
|
||||||
|
} => {$crate::paste::paste!{
|
||||||
|
// First we declare the function that the user gave us.
|
||||||
|
$(#[$meta])*
|
||||||
|
// XXXX mangling the types here is not good; see comment in example above.
|
||||||
|
$v async fn $name($obj: std::sync::Arc<$objtype>, $cmd: Box<$cmdtype>, $ctx: std::sync::Arc<dyn $crate::Context>) -> $rtype {
|
||||||
|
$($body)*
|
||||||
|
}
|
||||||
|
// Now we declare a type-erased version of the function that takes Arc<dyn> and Box<dyn> arguments, and returns
|
||||||
|
// a boxed future.
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn [<_typeerased_ $name>](obj: std::sync::Arc<dyn $crate::Object>, cmd: Box<dyn $crate::Command>, ctx: std::sync::Arc<dyn $crate::Context>) -> $crate::futures::future::BoxFuture<'static, $rtype> {
|
||||||
|
use $crate::futures::FutureExt;
|
||||||
|
let obj = obj
|
||||||
|
.downcast_arc::<$objtype>()
|
||||||
|
.unwrap_or_else(|_| panic!());
|
||||||
|
let cmd = cmd
|
||||||
|
.downcast::<$cmdtype>()
|
||||||
|
.unwrap_or_else(|_| panic!());
|
||||||
|
$name(obj, cmd, ctx).boxed()
|
||||||
|
}
|
||||||
|
// Finally we use `inventory` to register the type-erased function with
|
||||||
|
// the right types.
|
||||||
|
$crate::inventory::submit!{
|
||||||
|
$crate::dispatch::InvokeEntry_::new(
|
||||||
|
<$objtype as $crate::typeid::HasConstTypeId_>::CONST_TYPE_ID_,
|
||||||
|
<$cmdtype as $crate::typeid::HasConstTypeId_>::CONST_TYPE_ID_,
|
||||||
|
[<_typeerased_ $name >]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$(rpc_invoke_fn!{$($more)+})?
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actual types to use when looking up a function in our HashMap.
|
||||||
|
#[derive(Eq, PartialEq, Clone, Debug, Hash)]
|
||||||
|
struct FuncType {
|
||||||
|
obj_id: ConstTypeId_,
|
||||||
|
cmd_id: ConstTypeId_,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Table mapping `FuncType` to `ErasedInvokeFn`.
|
||||||
|
///
|
||||||
|
/// This is constructed once, the first time we use our dispatch code.
|
||||||
|
static FUNCTION_TABLE: Lazy<HashMap<FuncType, ErasedInvokeFn>> = Lazy::new(|| {
|
||||||
|
// We want to assert that there are no duplicates, so we can't use "collect"
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for ent in inventory::iter::<InvokeEntry_>() {
|
||||||
|
let InvokeEntry_ {
|
||||||
|
obj_id,
|
||||||
|
cmd_id,
|
||||||
|
func,
|
||||||
|
} = *ent;
|
||||||
|
let old_val = map.insert(FuncType { obj_id, cmd_id }, func);
|
||||||
|
assert!(
|
||||||
|
old_val.is_none(),
|
||||||
|
"Tried to register two RPC functions with the same type IDs!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
map
|
||||||
|
});
|
||||||
|
|
||||||
|
/// An error that occurred while trying to invoke a command on an object.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum InvokeError {
|
||||||
|
/// There is no implementation for the given combination of object
|
||||||
|
/// type and command type.
|
||||||
|
#[error("No implementation for provided object and command types.")]
|
||||||
|
NoImpl,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to find an appropriate function for calling a given RPC command on a
|
||||||
|
/// given RPC-visible object.
|
||||||
|
///
|
||||||
|
/// On success, return a Future.
|
||||||
|
pub fn invoke_command(
|
||||||
|
obj: Arc<dyn Object>,
|
||||||
|
cmd: Box<dyn Command>,
|
||||||
|
ctx: Arc<dyn Context>,
|
||||||
|
) -> Result<RpcResultFuture, InvokeError> {
|
||||||
|
let func_type = FuncType {
|
||||||
|
obj_id: obj.const_type_id(),
|
||||||
|
cmd_id: cmd.const_type_id(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let func = FUNCTION_TABLE.get(&func_type).ok_or(InvokeError::NoImpl)?;
|
||||||
|
|
||||||
|
Ok(func(obj, cmd, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use futures_await_test::async_test;
|
||||||
|
|
||||||
|
pub struct Animal {}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct SayHi {}
|
||||||
|
crate::impl_const_type_id!(Animal SayHi);
|
||||||
|
impl crate::Object for Animal {}
|
||||||
|
#[typetag::deserialize]
|
||||||
|
impl crate::Command for SayHi {}
|
||||||
|
|
||||||
|
rpc_invoke_fn! {
|
||||||
|
/// Hello there
|
||||||
|
async fn invoke(_obj: Animal, cmd: SayHi, _ctx) -> String {
|
||||||
|
format!("{:?}", cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Ctx {}
|
||||||
|
impl crate::Context for Ctx {
|
||||||
|
fn lookup_object(
|
||||||
|
&self,
|
||||||
|
_id: &crate::ObjectId,
|
||||||
|
) -> Option<std::sync::Arc<dyn crate::Object>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO RPC: Improve this test!
|
||||||
|
#[async_test]
|
||||||
|
async fn t() {
|
||||||
|
use super::*;
|
||||||
|
let animal: Arc<dyn Object> = Arc::new(Animal {});
|
||||||
|
let hi: Box<dyn Command> = Box::new(SayHi {});
|
||||||
|
let ctx = Arc::new(Ctx {});
|
||||||
|
let s = invoke_command(animal, hi, ctx).unwrap().await;
|
||||||
|
assert_eq!(&s, "SayHi");
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,61 @@
|
||||||
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
|
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
|
||||||
|
|
||||||
mod cmd;
|
mod cmd;
|
||||||
|
pub mod dispatch;
|
||||||
mod obj;
|
mod obj;
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod typeid;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use cmd::Command;
|
pub use cmd::Command;
|
||||||
|
pub use dispatch::invoke_command;
|
||||||
pub use obj::{Object, ObjectId};
|
pub use obj::{Object, ObjectId};
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use {downcast_rs, futures, inventory, paste};
|
||||||
|
|
||||||
|
/// An error returned from [`ContextExt::lookup`].
|
||||||
|
///
|
||||||
|
/// TODO RPC: This type should be made to conform with however we represent RPC
|
||||||
|
/// errors.
|
||||||
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum LookupError {
|
||||||
|
/// The specified object does not (currently) exist,
|
||||||
|
/// or the user does not have permission to access it.
|
||||||
|
#[error("No visible object with ID {0:?}")]
|
||||||
|
NoObject(ObjectId),
|
||||||
|
|
||||||
|
/// The specified object exists, but does not have the
|
||||||
|
/// expected type.
|
||||||
|
#[error("Unexpected type on object with ID {0:?}")]
|
||||||
|
WrongType(ObjectId),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait describing the context in which an RPC command is executed.
|
||||||
|
pub trait Context: Send + Sync {
|
||||||
|
/// Look up an object by identity within this context.
|
||||||
|
///
|
||||||
|
/// A return of `None` may indicate that the object has disappeared,
|
||||||
|
/// that the object doesn't exist,
|
||||||
|
/// that the [`ObjectId`] is ill-formed,
|
||||||
|
/// or that the user has no permission to access the object.
|
||||||
|
fn lookup_object(&self, id: &ObjectId) -> Option<Arc<dyn Object>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait for [`Context`].
|
||||||
|
///
|
||||||
|
/// This is a separate trait so that `Context` can be object-safe.
|
||||||
|
pub trait ContextExt: Context {
|
||||||
|
/// Look up an object of a given type, and downcast it.
|
||||||
|
///
|
||||||
|
/// Return an error if the object can't be found, or has the wrong type.
|
||||||
|
fn lookup<T: Object>(&self, id: &ObjectId) -> Result<Arc<T>, LookupError> {
|
||||||
|
self.lookup_object(id)
|
||||||
|
.ok_or_else(|| LookupError::NoObject(id.clone()))?
|
||||||
|
.downcast_arc()
|
||||||
|
.map_err(|_| LookupError::WrongType(id.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: Context> ContextExt for T {}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
use downcast_rs::DowncastSync;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub trait Object {}
|
use crate::typeid::GetConstTypeId_;
|
||||||
|
|
||||||
|
pub trait Object: GetConstTypeId_ + DowncastSync {}
|
||||||
|
downcast_rs::impl_downcast!(sync Object);
|
||||||
|
|
||||||
/// An identifier for an Object within the context of a Session.
|
/// An identifier for an Object within the context of a Session.
|
||||||
///
|
///
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
//! A kludgy replacement for [`std::any::TypeId`] that can be used in a constant context.
|
||||||
|
|
||||||
|
/// A less helpful variant of `std::any::TypeId` that can be used in a const
|
||||||
|
/// context.
|
||||||
|
///
|
||||||
|
/// Until the [relevant Rust feature] is stabilized, it's not possible to get a
|
||||||
|
/// TypeId for a type and store it in a const. But sadly, we need to do so for
|
||||||
|
/// our dispatch code.
|
||||||
|
///
|
||||||
|
/// Thus, we use a nasty hack: we use the address of the function
|
||||||
|
/// `TypeId::of::<T>` as the identifier for the type of T.
|
||||||
|
///
|
||||||
|
/// This type and the module containing it are hidden: Nobody should actually
|
||||||
|
/// use it outside of our dispatch code. Once we can use `TypeId` instead, we
|
||||||
|
/// should and will.
|
||||||
|
///
|
||||||
|
/// To make a type participate in this system, use the [`impl_const_type_id`]
|
||||||
|
/// macro.
|
||||||
|
///
|
||||||
|
/// **Do not mention this type outside of this module.**
|
||||||
|
///
|
||||||
|
/// [relevant Rust feature]: https://github.com/rust-lang/rust/issues/77125
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
|
||||||
|
pub struct ConstTypeId_(
|
||||||
|
/// Sadly this has to be `pub` so we can construct these from other crates.
|
||||||
|
///
|
||||||
|
/// We could make a constructor, but there is no point.
|
||||||
|
pub *const (),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Safety: We never actually access the pointer.
|
||||||
|
unsafe impl Send for ConstTypeId_ {}
|
||||||
|
// Safety: We never actually access the pointer.
|
||||||
|
unsafe impl Sync for ConstTypeId_ {}
|
||||||
|
|
||||||
|
/// An object for which we can access a [`ConstTypeId_`] dynamically.
|
||||||
|
///
|
||||||
|
/// **Do not mention this type outside of this module.**
|
||||||
|
pub trait GetConstTypeId_ {
|
||||||
|
fn const_type_id(&self) -> ConstTypeId_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An object for which we can get a [`ConstTypeId_`] at compile time.
|
||||||
|
///
|
||||||
|
/// This is precisely the functionality that [`std::any::TypeId`] doesn't
|
||||||
|
/// currently have.
|
||||||
|
///
|
||||||
|
/// **Do not mention this type outside of this module.**
|
||||||
|
pub trait HasConstTypeId_ {
|
||||||
|
const CONST_TYPE_ID_: ConstTypeId_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement [`GetConstTypeId_`] and [`HasConstTypeId_`] for one or more types.
|
||||||
|
///
|
||||||
|
/// To avoid truly unpleasant consequences, this macro only works on simple
|
||||||
|
/// identifiers, so you can't run it on arbitrary types, or on types in other
|
||||||
|
/// modules.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! impl_const_type_id {
|
||||||
|
{ $($type:ident)* } => {
|
||||||
|
$(
|
||||||
|
impl $crate::typeid::HasConstTypeId_ for $type {
|
||||||
|
const CONST_TYPE_ID_: $crate::typeid::ConstTypeId_ = $crate::typeid::ConstTypeId_(
|
||||||
|
std::any::TypeId::of::<$type> as *const ()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $crate::typeid::GetConstTypeId_ for $type {
|
||||||
|
fn const_type_id(&self) -> $crate::typeid::ConstTypeId_ {
|
||||||
|
<$type as $crate::typeid::HasConstTypeId_>::CONST_TYPE_ID_
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub use impl_const_type_id;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use assert_impl::assert_impl;
|
||||||
|
|
||||||
|
struct Foo(usize);
|
||||||
|
struct Bar {}
|
||||||
|
|
||||||
|
crate::impl_const_type_id! {Foo Bar}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typeid_basics() {
|
||||||
|
use super::*;
|
||||||
|
assert_impl!(Send: ConstTypeId_);
|
||||||
|
assert_impl!(Sync: ConstTypeId_);
|
||||||
|
let foo1 = Foo(3);
|
||||||
|
let foo2 = Foo(4);
|
||||||
|
let bar = Bar {};
|
||||||
|
|
||||||
|
assert_eq!(foo1.const_type_id(), foo2.const_type_id());
|
||||||
|
assert_ne!(foo1.const_type_id(), bar.const_type_id());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue