diff --git a/cpp/src/lib.rs b/cpp/src/lib.rs index 4e476f6..549006f 100644 --- a/cpp/src/lib.rs +++ b/cpp/src/lib.rs @@ -109,3 +109,59 @@ macro_rules! cpp { } }; } + +#[doc(hidden)] +pub trait CppTrait { + type BaseType; + const ARRAY_SIZE: usize; + const CPP_TYPE: &'static str; +} + +/// This macro allow to wrap a relocatable C++ struct or class that might +/// have destructor or copy constructor, and instantiate the Drop and Clone +/// trait appropriately. +/// +/// Warning: This only work if the C++ class that are relocatable, i.e., that +/// can be moved in memory using memmove. +/// This disallows most classes from the standard library. +/// This restriction exists because rust is allowed to move your types around. +/// Most C++ types that do not contain self-references or +/// +/// ```ignore +/// cpp_class!(pub struct MyClass as "MyClass"); +/// impl MyClass { +/// fn new() -> Self { +/// unsafe { cpp!([] -> MyClass as "MyClass" { return MyClass(); }) } +/// } +/// fn member_function(&self, param : i32) -> i32 { +/// unsafe { cpp!([self as "const MyClass*", param as "int"] -> i32 as "int" { +/// return self->member_function(param); +/// }) } +/// } +/// } +/// ``` +/// +/// This will create a rust struct MyClass, which has the same size and +/// alignment as the the C++ class "MyClass". It will also implement the Drop trait +/// calling the destructor, the Clone trait calling the copy constructor, if the +/// class is copyable (or Copy if it is trivially copyable), and Default if the class +/// is default constructible +/// +#[macro_export] +macro_rules! cpp_class { + (struct $name:ident as $type:expr) => { + #[derive(__cpp_internal_class)] + #[repr(C)] + struct $name { + _opaque : [<$name as $crate::CppTrait>::BaseType ; <$name as $crate::CppTrait>::ARRAY_SIZE + (stringify!(struct $name as $type), 0).1] + } + }; + (pub struct $name:ident as $type:expr) => { + #[derive(__cpp_internal_class)] + #[repr(C)] + pub struct $name { + _opaque : [<$name as $crate::CppTrait>::BaseType ; <$name as $crate::CppTrait>::ARRAY_SIZE + (stringify!(pub struct $name as $type), 0).1] + } + }; +} + diff --git a/cpp_build/src/lib.rs b/cpp_build/src/lib.rs index 3f5b799..67be96d 100644 --- a/cpp_build/src/lib.rs +++ b/cpp_build/src/lib.rs @@ -23,8 +23,8 @@ use std::fs::{create_dir, remove_dir_all, File}; use std::io::prelude::*; use syn::visit::Visitor; use syn::{Mac, Span, Spanned, DUMMY_SPAN}; -use cpp_common::{parsing, Capture, Closure, ClosureSig, Macro, LIB_NAME, STRUCT_METADATA_MAGIC, - VERSION}; +use cpp_common::{parsing, Capture, Closure, ClosureSig, Macro, Class, LIB_NAME, STRUCT_METADATA_MAGIC, + VERSION, flags}; use cpp_synmap::SourceMap; fn warnln_impl(a: String) { @@ -44,7 +44,30 @@ const INTERNAL_CPP_STRUCTS: &'static str = r#" #include "stdint.h" // For {u}intN_t #include // For placement new +#include // For abort +#include +namespace rustcpp { +template +typename std::enable_if::value>::type copy_helper(const void *src, void *dest) +{ new (dest) T (*static_cast(src)); } +template +typename std::enable_if::value>::type copy_helper(const void *, void *) +{ std::abort(); } +template +typename std::enable_if::value>::type default_helper(void *dest) +{ new (dest) T(); } +template +typename std::enable_if::value>::type default_helper(void *) +{ std::abort(); } +} + +#define RUST_CPP_CLASS_HELPER(HASH, ...) \ + extern "C" { \ + void __cpp_destructor_##HASH(void *ptr) { typedef __VA_ARGS__ T; static_cast(ptr)->~T(); } \ + void __cpp_copy_##HASH(const void *src, void *dest) { rustcpp::copy_helper<__VA_ARGS__>(src, dest); } \ + void __cpp_default_##HASH(void *dest) { rustcpp::default_helper<__VA_ARGS__>(dest); } \ + } "#; lazy_static! { @@ -89,21 +112,23 @@ fn gen_cpp_lib(visitor: &Handle) -> PathBuf { // Generate the sizes array with the sizes of each of the argument types if is_void { sizealign.push(format!( - "{{{hash}ull, 0, 1}}", + "{{{hash}ull, 0, 1, 0}}", hash = hash )); } else { sizealign.push(format!("{{ {hash}ull, sizeof({type}), - rustcpp::AlignOf<{type}>::value + rustcpp::AlignOf<{type}>::value, + rustcpp::Flags<{type}>::value }}", hash=hash, type=cpp)); } for &Capture { ref cpp, .. } in captures { sizealign.push(format!("{{ {hash}ull, sizeof({type}), - rustcpp::AlignOf<{type}>::value + rustcpp::AlignOf<{type}>::value, + rustcpp::Flags<{type}>::value }}", hash=hash, type=cpp)); } @@ -167,6 +192,26 @@ void {name}({params}{comma} void* __result) {{ } } + for class in &visitor.classes { + let hash = class.name_hash(); + + // Generate the sizes array + sizealign.push(format!("{{ + {hash}ull, + sizeof({type}), + rustcpp::AlignOf<{type}>::value, + rustcpp::Flags<{type}>::value + }}", hash=hash, type=class.cpp)); + + // Generate helper function. + // (this is done in a macro, which right after a #line directing pointing to the location of + // the cpp_class! macro in order to give right line information in the possible errors) + write!( + output, "{line}RUST_CPP_CLASS_HELPER({hash}, {cpp_name})\n", + line = class.line, hash = hash, cpp_name = class.cpp + ).unwrap(); + } + let mut magic = vec![]; for mag in STRUCT_METADATA_MAGIC.iter() { magic.push(format!("{}", mag)); @@ -187,10 +232,24 @@ struct AlignOf {{ static const uintptr_t value = sizeof(Inner) - sizeof(T); }}; +template +struct Flags {{ + static const uintptr_t value = + (std::is_copy_constructible::value << {flag_is_copy_constructible}) | + (std::is_default_constructible::value << {flag_is_default_constructible}) | +#if !defined(__GNUC__) || (__GNUC__ + 0 >= 5) || defined(__clang__) + (std::is_trivially_destructible::value << {flag_is_trivially_destructible}) | + (std::is_trivially_copyable::value << {flag_is_trivially_copyable}) | + (std::is_trivially_default_constructible::value << {flag_is_trivially_default_constructible}) | +#endif + 0; +}}; + struct SizeAlign {{ uint64_t hash; uint64_t size; uint64_t align; + uint64_t flags; }}; struct MetaData {{ @@ -212,7 +271,12 @@ MetaData metadata = {{ data = sizealign.join(", "), length = sizealign.len(), magic = magic.join(", "), - version = VERSION + version = VERSION, + flag_is_copy_constructible = flags::IS_COPY_CONSTRUCTIBLE, + flag_is_default_constructible = flags::IS_DEFAULT_CONSTRUCTIBLE, + flag_is_trivially_destructible = flags::IS_TRIVIALLY_DESTRUCTIBLE, + flag_is_trivially_copyable = flags::IS_TRIVIALLY_COPYABLE, + flag_is_trivially_default_constructible = flags::IS_TRIVIALLY_DEFAULT_CONSTRUCTIBLE, ).unwrap(); result_path @@ -472,6 +536,7 @@ successfully, such that rustc can provide an error message."#, // Parse the macro definitions let mut visitor = Handle { closures: Vec::new(), + classes: Vec::new(), snippets: String::new(), sm: &sm, }; @@ -493,21 +558,26 @@ pub fn build>(path: P) { struct Handle<'a> { closures: Vec, + classes: Vec, snippets: String, sm: &'a SourceMap, } +fn line_directive(span: syn::Span, sm: &SourceMap) -> String { + let loc = sm.locinfo(span).unwrap(); + let mut line = format!("#line {} {:?}\n", loc.line, loc.path); + for _ in 0..loc.col { + line.push(' '); + } + return line; +} + fn extract_with_span(spanned: &mut Spanned, src: &str, offset: usize, sm: &SourceMap) { if spanned.span != DUMMY_SPAN { let src_slice = &src[spanned.span.lo..spanned.span.hi]; spanned.span.lo += offset; spanned.span.hi += offset; - - let loc = sm.locinfo(spanned.span).unwrap(); - spanned.node = format!("#line {} {:?}\n", loc.line, loc.path); - for _ in 0..loc.col { - spanned.node.push(' '); - } + spanned.node = line_directive(spanned.span, sm); spanned.node.push_str(src_slice); } } @@ -538,5 +608,18 @@ impl<'a> Visitor for Handle<'a> { } } } + if mac.path.segments[0].ident.as_ref() == "cpp_class" { + let tts = &mac.tts; + assert!(tts.len() >= 1); + let span = Span { + lo: tts[0].span().lo, + hi: tts[tts.len() - 1].span().hi, + }; + let src = self.sm.source_text(span).unwrap(); + let input = synom::ParseState::new(&src); + let mut class = parsing::class_macro(input).expect("cpp_class! macro"); + class.line = line_directive(span, self.sm); + self.classes.push(class); + } } } diff --git a/cpp_common/src/lib.rs b/cpp_common/src/lib.rs index 9b51b20..5c8b245 100644 --- a/cpp_common/src/lib.rs +++ b/cpp_common/src/lib.rs @@ -17,6 +17,14 @@ pub const VERSION: &'static str = env!("CARGO_PKG_VERSION"); pub const LIB_NAME: &'static str = "librust_cpp_generated.a"; pub const MSVC_LIB_NAME: &'static str = "rust_cpp_generated.lib"; +pub mod flags { + pub const IS_COPY_CONSTRUCTIBLE : u32 = 0; + pub const IS_DEFAULT_CONSTRUCTIBLE : u32 = 1; + pub const IS_TRIVIALLY_DESTRUCTIBLE : u32 = 2; + pub const IS_TRIVIALLY_COPYABLE : u32 = 3; + pub const IS_TRIVIALLY_DEFAULT_CONSTRUCTIBLE : u32 = 4; +} + /// This constant is expected to be a unique string within the compiled binary /// which preceeds a definition of the metadata. It begins with /// rustcpp~metadata, which is printable to make it easier to locate when @@ -28,7 +36,7 @@ pub const MSVC_LIB_NAME: &'static str = "rust_cpp_generated.lib"; pub const STRUCT_METADATA_MAGIC: [u8; 128] = [ b'r', b'u', b's', b't', b'c', b'p', b'p', b'~', b'm', b'e', b't', b'a', b'd', b'a', b't', b'a', - 91, 74, 112, 213, 165, 185, 214, 120, 179, 17, 185, 25, 182, 253, 82, 118, + 92, 74, 112, 213, 165, 185, 214, 120, 179, 17, 185, 25, 182, 253, 82, 118, 148, 29, 139, 208, 59, 153, 78, 137, 230, 54, 26, 177, 232, 121, 132, 166, 44, 106, 218, 57, 158, 33, 69, 32, 54, 204, 123, 226, 99, 117, 60, 173, 112, 61, 56, 174, 117, 141, 126, 249, 79, 159, 6, 119, 2, 129, 147, 66, @@ -71,6 +79,24 @@ pub struct Closure { pub body: Spanned, } +#[derive(Clone, Debug)] +pub struct Class { + pub name: Ident, + pub cpp: String, + pub public: bool, + pub line: String, // the #line directive +} + +impl Class { + pub fn name_hash(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.name.hash(&mut hasher); + self.cpp.hash(&mut hasher); + self.public.hash(&mut hasher); + hasher.finish() + } +} + pub enum Macro { Closure(Closure), Lit(Spanned), @@ -78,8 +104,8 @@ pub enum Macro { pub mod parsing { use syn::parse::{ident, string, tt, ty}; - use syn::{Spanned, Ty, DUMMY_SPAN}; - use super::{Capture, Closure, ClosureSig, Macro}; + use syn::{Spanned, Ty, DUMMY_SPAN, Ident}; + use super::{Capture, Closure, ClosureSig, Macro, Class}; macro_rules! mac_body { ($i: expr, $submac:ident!( $($args:tt)* )) => { @@ -95,10 +121,12 @@ pub mod parsing { }; } + named!(ident_or_self -> Ident, alt!( ident | keyword!("self") => { |_| "self".into() } )); + named!(name_as_string -> Capture, do_parse!( is_mut: option!(keyword!("mut")) >> - id: ident >> + id: ident_or_self >> keyword!("as") >> cty: string >> (Capture { @@ -168,4 +196,20 @@ pub mod parsing { map!(tuple!( punct!("@"), keyword!("TYPE"), cpp_closure ), (|(_, _, x)| x)))); + + named!(pub cpp_class -> Class, + do_parse!( + is_pub: option!(keyword!("pub")) >> + keyword!("struct") >> + name: ident >> + keyword!("as") >> + cpp_type: string >> + (Class { + name: name, + cpp: cpp_type.value, + public: is_pub.is_some(), + line: String::default(), + }))); + + named!(pub class_macro -> Class , mac_body!(cpp_class)); } diff --git a/cpp_macros/src/lib.rs b/cpp_macros/src/lib.rs index a63dfcc..ba84d71 100644 --- a/cpp_macros/src/lib.rs +++ b/cpp_macros/src/lib.rs @@ -26,7 +26,7 @@ use std::env; use std::path::PathBuf; use std::collections::HashMap; use proc_macro::TokenStream; -use cpp_common::{parsing, LIB_NAME, MSVC_LIB_NAME, VERSION}; +use cpp_common::{parsing, LIB_NAME, MSVC_LIB_NAME, VERSION, flags}; use syn::Ident; use std::io::{self, BufReader, Read, Seek, SeekFrom}; @@ -34,6 +34,17 @@ use std::fs::File; use aho_corasick::{AcAutomaton, Automaton}; use byteorder::{LittleEndian, ReadBytesExt}; +struct MetaData { + size: usize, + align: usize, + flags: u64 +} +impl MetaData { + fn has_flag(&self, f : u32) -> bool { + self.flags & (1 << f) != 0 + } +} + lazy_static! { static ref OUT_DIR: PathBuf = PathBuf::from(env::var("OUT_DIR").expect(r#" @@ -42,7 +53,7 @@ lazy_static! { The OUT_DIR environment variable was not set. NOTE: rustc must be run by Cargo."#)); - static ref METADATA: HashMap> = { + static ref METADATA: HashMap> = { let file = open_lib_file().expect(r#" -- rust-cpp fatal error -- @@ -58,7 +69,7 @@ I/O error while reading metadata from target library file."#) } /// NOTE: This panics when it can produce a better error message -fn read_metadata(file: File) -> io::Result>> { +fn read_metadata(file: File) -> io::Result>> { let mut file = BufReader::new(file); let end = { const AUTO_KEYWORD: &'static [&'static [u8]] = &[&cpp_common::STRUCT_METADATA_MAGIC]; @@ -94,18 +105,19 @@ Version mismatch between cpp_macros and cpp_build for same crate."# ); let length = file.read_u64::()?; - let mut size_data = HashMap::new(); + let mut metadata = HashMap::new(); for _ in 0..length { let hash = file.read_u64::()?; let size = file.read_u64::()? as usize; let align = file.read_u64::()? as usize; + let flags = file.read_u64::()? as u64; - size_data + metadata .entry(hash) .or_insert(Vec::new()) - .push((size, align)); + .push(MetaData{size, align, flags}); } - Ok(size_data) + Ok(metadata) } /// Try to open a file handle to the lib file. This is used to scan it for @@ -195,7 +207,7 @@ NOTE: They cannot be generated by macro expansion."#, // Generate the assertion to check that the size and align of the types // match before calling. - let (size, align) = size_data[i + 1]; + let MetaData { size, align, .. } = size_data[i + 1]; let sizeof_msg = format!( "size_of for argument `{}` does not match between c++ and \ rust", @@ -231,7 +243,9 @@ NOTE: They cannot be generated by macro expansion."#, quote!(*const) }; - extern_params.push(quote!(#written_name : #ptr u8)); + let arg_name : Ident = format!("arg_{}", written_name).into(); + + extern_params.push(quote!(#arg_name : #ptr u8)); tt_args.push(quote!(#mb_mut #mac_name : ident as #mac_cty : tt)); @@ -243,7 +257,7 @@ NOTE: They cannot be generated by macro expansion."#, let extern_name = closure.extern_name(); let ret_ty = &closure.ret; - let (ret_size, ret_align) = size_data[0]; + let (ret_size, ret_align) = (size_data[0].size, size_data[0].align); let is_void = closure.cpp == "void"; let decl = if is_void { @@ -295,3 +309,115 @@ NOTE: They cannot be generated by macro expansion."#, result.to_string().parse().unwrap() } + +#[proc_macro_derive(__cpp_internal_class)] +pub fn expand_wrap_class(input: TokenStream) -> TokenStream { + let source = input.to_string(); + + #[cfg_attr(rustfmt, rustfmt_skip)] + const SUFFIX: &'static [&'static str] = &[ + ")", ",", "0", ")", ".", "1", "]", ",", "}" + ]; + + let s = source.find("stringify!(").expect("expected 'strignify!' token in class content") + 11; + let mut tokens : &str = &source[s..].trim(); + + for token in SUFFIX.iter().rev() { + assert!( + tokens.ends_with(token), + "expected suffix token {}, got {}", + token, + tokens + ); + tokens = &tokens[..tokens.len() - token.len()].trim(); + } + + let class = parsing::cpp_class(synom::ParseState::new(tokens)) + .expect("cpp_class! macro"); + + let hash = class.name_hash(); + + let size_data = METADATA.get(&hash).expect( + r#" +-- rust-cpp fatal error -- + +This cpp_class! macro is not found in the library's rust-cpp metadata. +NOTE: Only cpp_class! macros found directly in the program source will be parsed - +NOTE: They cannot be generated by macro expansion."#, + ); + + let (size, align) = (size_data[0].size, size_data[0].align); + + let base_type = match align { + 1 => { quote!(u8) } + 2 => { quote!(u16) } + 4 => { quote!(u32) } + 8 => { quote!(u64) } + _ => { panic!("unsupported alignment") } + }; + + let destructor_name : Ident = format!("__cpp_destructor_{}", hash).into(); + let copyctr_name : Ident = format!("__cpp_copy_{}", hash).into(); + let defaultctr_name : Ident = format!("__cpp_default_{}", hash).into(); + let class_name = class.name; + + let mut result = quote! { + impl ::cpp::CppTrait for #class_name { + type BaseType = #base_type; + const ARRAY_SIZE: usize = #size / #align; + const CPP_TYPE: &'static str = stringify!(#class_name); + } + }; + if !size_data[0].has_flag(flags::IS_TRIVIALLY_DESTRUCTIBLE) { + result = quote!{ #result + impl Drop for #class_name { + fn drop(&mut self) { + unsafe { + extern "C" { fn #destructor_name(_: *mut #class_name); } + #destructor_name(&mut *self); + } + } + } + }; + }; + if size_data[0].has_flag(flags::IS_COPY_CONSTRUCTIBLE) { + if !size_data[0].has_flag(flags::IS_TRIVIALLY_COPYABLE) { + result = quote!{ #result + impl Clone for #class_name { + fn clone(&self) -> Self { + unsafe { + extern "C" { fn #copyctr_name(src: *const #class_name, dst: *mut #class_name); } + let mut ret : Self = std::mem::uninitialized(); + #copyctr_name(& *self, &mut ret); + ret + } + } + } + }; + } else { + result = quote!{ #result + impl Copy for #class_name { } + impl Clone for #class_name { + fn clone(&self) -> Self { *self } + } + }; + }; + } + if size_data[0].has_flag(flags::IS_DEFAULT_CONSTRUCTIBLE) { + result = quote!{ #result + impl Default for #class_name { + fn default() -> Self { + unsafe { + extern "C" { fn #defaultctr_name(dst: *mut #class_name); } + let mut ret : Self = std::mem::uninitialized(); + #defaultctr_name(&mut ret); + ret + } + } + } + }; + } + + result.to_string().parse().unwrap() +} + diff --git a/test/src/header.h b/test/src/header.h index 874f1e6..3f30346 100644 --- a/test/src/header.h +++ b/test/src/header.h @@ -1,12 +1,58 @@ #ifndef header_h__ #define header_h__ +#include +#include + +#if __cplusplus > 199711L +#include +typedef std::atomic counter_t; +#define COUNTER_STATIC static +#else +typedef int counter_t; +#endif + +// This counter is incremented by destructors and constructors +// and must be 0 at the end of the program +inline counter_t &counter() { + static counter_t counter; + struct CheckCounter { + ~CheckCounter() { + assert(counter == 0); + } + }; + static CheckCounter checker; + return counter; +} + +// class with destructor and copy constructor class A { public: int a; int b; - A(int a, int b) : a(a), b(b) {} - ~A() {} + A(int a, int b) : a(a), b(b) { counter()++; } + A(const A &cpy) : a(cpy.a), b(cpy.b) { counter()++; } + ~A() { counter()--; } + void setValues(int _a, int _b) { a = _a; b = _b; } + int multiply() const { return a * b; } +}; + +// Simple struct without a destructor or copy constructor +struct B { + int a; + int b; }; +struct MoveOnly { + MoveOnly(int a = 8, int b = 9) : data(a,b) { } +#if !defined (_MSC_VER) || (_MSC_VER + 0 >= 1900) + MoveOnly(const MoveOnly &) = delete ; + MoveOnly& operator=(const MoveOnly &) = delete ; + MoveOnly(MoveOnly &&other) : data(other.data) { } + MoveOnly& operator=(MoveOnly &&other) { data = other.data; return *this; } +#endif + A data; +}; + + #endif // defined(header_h__) diff --git a/test/src/lib.rs b/test/src/lib.rs index f2d8c6d..a37ecaf 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -16,13 +16,38 @@ cpp!{{ #define _USE_MATH_DEFINES #include #include "src/header.h" + #include + #include }} -#[repr(C)] -struct A { - _opaque: [i32; 2], +cpp_class!(struct A as "A"); +impl A { + fn new(a : i32, b: i32) -> Self { + unsafe { + return cpp!([a as "int", b as "int"] -> A as "A" { + return A(a, b); + }); + } + } + + fn set_values(&mut self, a : i32, b: i32) { + unsafe { + return cpp!([self as "A*", a as "int", b as "int"] { + self->setValues(a, b); + }); + } + } + + fn multiply(&self) -> i32 { + unsafe { + return cpp!([self as "const A*"] -> i32 as "int" { + return self->multiply(); + }); + } + } } + #[test] fn captures() { let x: i32 = 10; @@ -86,12 +111,93 @@ fn destructor() { return A(5, 10); }); + let a1 = a.clone(); + let first = cpp!([a as "A"] -> i32 as "int32_t" { return a.a; }); assert_eq!(first, 5); + + let second = cpp!([a1 as "A"] -> i32 as "int32_t" { + return a1.b; + }); + + assert_eq!(second, 10); + } +} + +#[test] +fn member_function() { + let mut a = A::new(2,3); + assert_eq!(a.multiply(), 2*3); + + a.set_values(5,6); + assert_eq!(a.multiply(), 5*6); +} + +cpp_class!(struct B as "B"); +impl B { + fn new(a : i32, b: i32) -> Self { + unsafe { + return cpp!([a as "int", b as "int"] -> B as "B" { + B ret = { a, b }; + return ret; + }); + } + } + fn a(&mut self) -> &mut i32 { + unsafe { + return cpp!([self as "B*"] -> &mut i32 as "int*" { + return &self->a; + }); + } + } + fn b(&mut self) -> &mut i32 { + unsafe { + return cpp!([self as "B*"] -> &mut i32 as "int*" { + return &self->b; + }); + } + } +} + + +#[test] +fn simple_class() { + let mut b = B::new(12,34); + assert_eq!(*b.a(), 12); + assert_eq!(*b.b(), 34); + *b.a() = 45; + let mut b2 = b; + assert_eq!(*b2.a(), 45); + + let mut b3 = B::default(); + assert_eq!(*b3.a(), 0); + assert_eq!(*b3.b(), 0); +} + +#[test] +fn move_only() { + cpp_class!(struct MoveOnly as "MoveOnly"); + impl MoveOnly { + fn data(&self) -> &A { + unsafe { + return cpp!([self as "MoveOnly*"] -> &A as "A*" { + return &self->data; + }); + } + } } + let mo1 = MoveOnly::default(); + assert_eq!(mo1.data().multiply(), 8*9); + let mut mo2 = mo1; + let mo3 = unsafe { cpp!([mut mo2 as "MoveOnly"] -> MoveOnly as "MoveOnly" { + mo2.data.a = 7; + return MoveOnly(3,2); + })}; + assert_eq!(mo2.data().multiply(), 7*9); + assert_eq!(mo3.data().multiply(), 3*2); } #[test]