#[macro_export]
macro_rules! class {
($class: ident) => {
#[repr(C)]
#[derive(Debug, PartialEq)]
pub struct $class {
value: $crate::types::Value,
}
impl From<$crate::types::Value> for $class {
fn from(value: $crate::types::Value) -> Self {
$class { value: value }
}
}
impl $crate::Object for $class {
#[inline]
fn value(&self) -> $crate::types::Value {
self.value
}
}
};
($class: ident, $path: literal) => {
$crate::class!($class);
impl $crate::VerifiedObject for $class {
fn is_correct_type<T: $crate::Object>(object: &T) -> bool {
$crate::Class::from_path($path)
.map(|class| class.case_equals(object))
.unwrap_or(false)
}
fn error_message() -> &'static str {
concat!("Error converting to ", $path)
}
}
};
}
#[macro_export]
macro_rules! module {
($module: ident) => {
#[repr(C)]
#[derive(Debug, PartialEq)]
pub struct $module {
value: $crate::types::Value,
}
impl From<$crate::types::Value> for $module {
fn from(value: $crate::types::Value) -> Self {
$module { value: value }
}
}
impl $crate::Object for $module {
#[inline]
fn value(&self) -> $crate::types::Value {
self.value
}
}
};
}
#[macro_export]
macro_rules! unsafe_methods {
(
$rtself_class: ty,
$rtself_name: ident,
$(
fn $method_name: ident
($($arg_name: ident: $arg_type: ty),* $(,)?) -> $return_type: ty $body: block
$(,)?
)*
) => {
$(
$crate::rutie_callback! {
pub fn $method_name(argc: $crate::types::Argc,
argv: *const $crate::AnyObject,
#[allow(unused_mut)]
#[allow(unused_variables)]
mut $rtself_name: $rtself_class)
-> <$return_type as $crate::MethodReturn>::Output {
let result = (move || -> $return_type {
let _arguments = $crate::util::parse_arguments(argc, argv);
let mut _i = 0;
$(
let $arg_name = unsafe {
<$crate::AnyObject as $crate::Object>
::to::<$arg_type>(&_arguments[_i])
};
_i += 1;
)*
$body
})();
$crate::util::method_return(result)
}
}
)*
}
}
#[macro_export]
macro_rules! methods {
(@method $rtself_class: ty, $rtself_name: ident, $method_name: ident,
($($arg_name: ident: $arg_type: ty),+ , * $splat_name: ident $(,)?),
$return_type: ty, $body: block) => {
$crate::methods!(@define $rtself_class, $rtself_name, $method_name,
[$($arg_name: $arg_type),+], [$splat_name], $return_type, $body);
};
(@method $rtself_class: ty, $rtself_name: ident, $method_name: ident,
(* $splat_name: ident $(,)?), $return_type: ty, $body: block) => {
$crate::methods!(@define $rtself_class, $rtself_name, $method_name,
[], [$splat_name], $return_type, $body);
};
(@method $rtself_class: ty, $rtself_name: ident, $method_name: ident,
($($arg_name: ident: $arg_type: ty),* $(,)?), $return_type: ty, $body: block) => {
$crate::methods!(@define $rtself_class, $rtself_name, $method_name,
[$($arg_name: $arg_type),*], [], $return_type, $body);
};
(@define $rtself_class: ty, $rtself_name: ident, $method_name: ident,
[$($arg_name: ident: $arg_type: ty),*], [$($splat_name: ident)?],
$return_type: ty, $body: block) => {
$crate::rutie_callback! {
pub fn $method_name(argc: $crate::types::Argc,
argv: *const $crate::AnyObject,
#[allow(unused_mut)]
#[allow(unused_variables)]
mut $rtself_name: $rtself_class)
-> <$return_type as $crate::MethodReturn>::Output {
let result = (move || -> $return_type {
let _arguments = $crate::util::parse_arguments(argc, argv);
let mut _i = 0;
$(
let $arg_name =
_arguments
.get(_i)
.ok_or_else(|| {
<$crate::AnyException as $crate::Exception>::new("ArgumentError",
Some(&format!(
"Argument '{}: {}' not found for method '{}'",
stringify!($arg_name),
stringify!($arg_type),
stringify!($method_name)
))
)
}).and_then(|argument| {
<$crate::AnyObject as $crate::Object>
::try_convert_to::<$arg_type>(argument)
});
_i += 1;
)*
$(
let $splat_name: $crate::Array = _arguments.iter().skip(_i).cloned().collect();
)?
$body
})();
$crate::util::method_return(result)
}
}
};
(
$rtself_class: ty,
$rtself_name: ident,
$(
fn $method_name: ident
($($params: tt)*) -> $return_type: ty $body: block
$(,)?
)*
) => {
$(
$crate::methods!(@method $rtself_class, $rtself_name, $method_name,
($($params)*), $return_type, $body);
)*
}
}
#[macro_export]
macro_rules! wrappable_struct {
(@parse $struct_name: ty, $wrapper: ident, $static_name: ident,
{ $($mark: tt)* }, { $($size: tt)* }, $(,)?) => {
$crate::wrappable_struct!(@define $struct_name, $wrapper, $static_name,
{ $($mark)* }, { $($size)* });
};
(@parse $struct_name: ty, $wrapper: ident, $static_name: ident,
{ }, { $($size: tt)* }, , mark($object: ident) $body: block $($rest: tt)*) => {
$crate::wrappable_struct!(@parse $struct_name, $wrapper, $static_name,
{ $object $body }, { $($size)* }, $($rest)*);
};
(@parse $struct_name: ty, $wrapper: ident, $static_name: ident,
{ $($mark: tt)* }, { }, , size($object: ident) $body: block $($rest: tt)*) => {
$crate::wrappable_struct!(@parse $struct_name, $wrapper, $static_name,
{ $($mark)* }, { $object $body }, $($rest)*);
};
(@mark_function_pointer { }) => {
None as Option<extern "C" fn(*mut $crate::types::c_void)>
};
(@mark_function_pointer { $object: ident $body: block }) => {
Some(Self::mark as extern "C" fn(*mut $crate::types::c_void))
};
(@mark_function_definition $struct_name: ty, { }) => {};
(@mark_function_definition $struct_name: ty, { $object: ident $body: block }) => {
pub extern "C" fn mark(data: *mut $crate::types::c_void) {
let mut data = unsafe { (data as *mut $struct_name).as_mut() };
if let Some(ref mut $object) = data {
$body
}
}
};
(@size_function_pointer { }) => {
None as Option<extern "C" fn(*const $crate::types::c_void) -> $crate::types::size_t>
};
(@size_function_pointer { $object: ident $body: block }) => {
Some(Self::size as extern "C" fn(*const $crate::types::c_void) -> $crate::types::size_t)
};
(@size_function_definition $struct_name: ty, { }) => {};
(@size_function_definition $struct_name: ty, { $object: ident $body: block }) => {
pub extern "C" fn size(data: *const $crate::types::c_void) -> $crate::types::size_t {
let data = unsafe { (data as *const $struct_name).as_ref() };
match data {
Some($object) => {
let size: usize = $body;
size as $crate::types::size_t
}
None => 0,
}
}
};
(@define $struct_name: ty, $wrapper: ident, $static_name: ident,
{ $($mark: tt)* }, { $($size: tt)* }) => {
pub struct $wrapper<T> {
data_type: $crate::types::DataType,
_marker: ::std::marker::PhantomData<T>,
}
::lazy_static::lazy_static! {
pub static ref $static_name: $wrapper<$struct_name> = $wrapper::new();
}
impl<T> $wrapper<T> {
fn new() -> $wrapper<T> {
let name = concat!("Rutie/", stringify!($struct_name));
let name = $crate::util::str_to_cstring(name);
let reserved_bytes: [*mut $crate::types::c_void; 2] = [::std::ptr::null_mut(); 2];
let dmark = $crate::wrappable_struct!(@mark_function_pointer { $($mark)* });
let dsize = $crate::wrappable_struct!(@size_function_pointer { $($size)* });
let data_type = $crate::types::DataType {
wrap_struct_name: name.into_raw(),
parent: ::std::ptr::null(),
data: ::std::ptr::null_mut(),
flags: $crate::types::Value::from(0),
function: $crate::types::DataTypeFunction {
dmark: dmark,
dfree: Some($crate::typed_data::free::<T>),
dsize: dsize,
reserved: reserved_bytes,
},
};
$wrapper {
data_type: data_type,
_marker: ::std::marker::PhantomData,
}
}
$crate::wrappable_struct!(@mark_function_definition $struct_name, { $($mark)* });
$crate::wrappable_struct!(@size_function_definition $struct_name, { $($size)* });
}
unsafe impl<T> Sync for $wrapper<T> {}
impl<T> $crate::typed_data::DataTypeWrapper<T> for $wrapper<T> {
fn data_type(&self) -> &$crate::types::DataType {
&self.data_type
}
}
};
($struct_name: ty, $wrapper: ident, $static_name: ident $($tail: tt)*) => {
$crate::wrappable_struct!(@parse $struct_name, $wrapper, $static_name, { }, { }, $($tail)*);
};
}
#[macro_export]
macro_rules! eval {
($string_arg:expr) => {{
$crate::VM::eval($string_arg)
}};
($string_arg:expr, $binding_arg:expr) => {{
let eval_str: $crate::AnyObject = $crate::RString::from($string_arg).into();
let bndng: $crate::AnyObject = $binding_arg.into();
let arguments = &[eval_str, bndng];
$crate::Class::from_existing("Kernel").protect_send("eval", arguments)
}};
($string_arg:expr, $binding_arg:expr, $filename:expr) => {{
let eval_str: $crate::AnyObject = $crate::RString::from($string_arg).into();
let bndng: $crate::AnyObject = $binding_arg.into();
let filename: $crate::AnyObject = $crate::RString::from($filename).into();
let arguments = &[eval_str, bndng, filename];
$crate::Class::from_existing("Kernel").protect_send("eval", arguments)
}};
($string_arg:expr, $binding_arg:expr, $filename:expr, $linenumber:expr) => {{
let eval_str: $crate::AnyObject = $crate::RString::from($string_arg).into();
let bndng: $crate::AnyObject = $binding_arg.into();
let filename: $crate::AnyObject = $crate::RString::from($filename).into();
let linenumber: $crate::AnyObject = $crate::Integer::from($linenumber as i64).into();
let arguments = &[eval_str, bndng, filename, linenumber];
$crate::Class::from_existing("Kernel").protect_send("eval", arguments)
}};
}
#[cfg(test)]
mod tests {
use crate::{
AnyException, AnyObject, Array, Boolean, Class, Exception, Fixnum, Module, NilClass,
Object, RString, VerifiedObject, GC, VM,
};
use std::sync::atomic::{AtomicUsize, Ordering};
crate::class!(RutieDslSplat);
crate::class!(RutieDslInner, "RutieDslOuter::Inner");
crate::class!(RutieDslMissing, "RutieDslOuter::Nowhere");
crate::class!(RutieDslNotAClass, "RutieDslOuter");
crate::methods!(
RutieDslInner,
rtself,
fn dsl_same_kind(other: RutieDslInner) -> Boolean {
Boolean::new(other.is_ok())
}
);
crate::methods!(
RutieDslSplat,
rtself,
fn dsl_rest_only(*rest) -> Array {
rest
}
fn dsl_first_and_rest(first: Fixnum, *rest,) -> Array {
let mut result = Array::new();
result.push(first.map(|n| n.to_any_object()).unwrap_or_else(|e| e.to_any_object()));
result.push(rest);
result
}
fn dsl_plain(a: Fixnum, b: Fixnum) -> Fixnum {
Fixnum::new(a.unwrap().to_i64() + b.unwrap().to_i64())
}
);
crate::unsafe_methods!(
RutieDslSplat,
rtself,
fn dsl_unsafe_two(a: Fixnum, b: Fixnum) -> Fixnum {
Fixnum::new(a.to_i64() * b.to_i64())
}
);
crate::class!(RutieDslResult);
static DROPPED: AtomicUsize = AtomicUsize::new(0);
struct Resource(#[allow(dead_code)] String);
impl Drop for Resource {
fn drop(&mut self) {
DROPPED.fetch_add(1, Ordering::SeqCst);
}
}
crate::methods!(
RutieDslResult,
rtself,
fn dsl_result_raise(raise: Boolean) -> Result<NilClass, AnyException> {
let _resource = Resource(String::from("held"));
if raise?.to_bool() {
return Err(AnyException::new("StandardError", Some("kaboom")));
}
Ok(NilClass::new())
}
fn dsl_result_double(number: Fixnum, *rest) -> Result<Fixnum, AnyException> {
let _resource = Resource(String::from("held"));
Ok(Fixnum::new(number?.to_i64() * 2 + rest.length() as i64))
}
);
crate::unsafe_methods!(
RutieDslResult,
rtself,
fn dsl_unsafe_result(number: Fixnum) -> Result<Fixnum, AnyException> {
let _resource = Resource(String::from("held"));
match number.to_i64() {
0 => Err(AnyException::new("ZeroDivisionError", Some("divided by 0"))),
n => Ok(Fixnum::new(100 / n)),
}
}
);
pub struct Payload {
objects: Vec<AnyObject>,
extra: usize,
}
crate::wrappable_struct! {
Payload,
PayloadWrapper,
PAYLOAD_WRAPPER,
size(data) {
std::mem::size_of::<Payload>() + data.extra
},
mark(data) {
for object in &data.objects {
GC::mark(object);
}
},
}
pub struct Plain;
crate::wrappable_struct!(Plain, PlainWrapper, PLAIN_WRAPPER);
fn eval_array(code: &str) -> Array {
VM::eval(code).unwrap().try_convert_to::<Array>().unwrap()
}
#[test]
fn test_class_with_ruby_path() {
crate::on_ruby_thread(|| {
Module::new("RutieDslOuter").define(|outer| {
outer.define_nested_class("Inner", None).define(|klass| {
klass.def("same_kind?", dsl_same_kind);
});
});
let inner = VM::eval("RutieDslOuter::Inner.new").unwrap();
assert!(RutieDslInner::is_correct_type(&inner));
assert!(inner.try_convert_to::<RutieDslInner>().is_ok());
assert_eq!(
RutieDslInner::error_message(),
"Error converting to RutieDslOuter::Inner"
);
let same = VM::eval("RutieDslOuter::Inner.new.same_kind?(RutieDslOuter::Inner.new)").unwrap();
assert_eq!(same.try_convert_to::<Boolean>(), Ok(Boolean::new(true)));
let same = VM::eval("RutieDslOuter::Inner.new.same_kind?(:other)").unwrap();
assert_eq!(same.try_convert_to::<Boolean>(), Ok(Boolean::new(false)));
let string = RString::new_utf8("inner");
assert!(!RutieDslInner::is_correct_type(&string));
let error = string.try_convert_to::<RutieDslInner>().unwrap_err();
assert!(Class::type_error().case_equals(&error));
assert_eq!(error.message(), "Error converting to RutieDslOuter::Inner");
assert!(!RutieDslMissing::is_correct_type(&inner));
assert!(inner.try_convert_to::<RutieDslMissing>().is_err());
assert!(!RutieDslNotAClass::is_correct_type(&inner));
});
}
#[test]
fn test_methods_splat() {
crate::on_ruby_thread(|| {
Class::new("RutieDslSplat", None).define(|klass| {
klass.def("rest_only", dsl_rest_only);
klass.def("first_and_rest", dsl_first_and_rest);
klass.def("plain", dsl_plain);
klass.def("unsafe_two", dsl_unsafe_two);
});
assert_eq!(eval_array("RutieDslSplat.new.rest_only").length(), 0);
assert_eq!(
eval_array("RutieDslSplat.new.rest_only(1, 2, 3)").length(),
3
);
let result = eval_array("RutieDslSplat.new.first_and_rest(1, :a, :b)");
assert_eq!(result.at(0).try_convert_to::<Fixnum>(), Ok(Fixnum::new(1)));
assert_eq!(result.at(1).try_convert_to::<Array>().unwrap().length(), 2);
let result = eval_array("RutieDslSplat.new.first_and_rest");
assert!(Class::argument_error().case_equals(&result.at(0)));
assert_eq!(result.at(1).try_convert_to::<Array>().unwrap().length(), 0);
let sum = VM::eval("RutieDslSplat.new.plain(2, 3)").unwrap();
assert_eq!(sum.try_convert_to::<Fixnum>(), Ok(Fixnum::new(5)));
});
}
#[test]
fn test_unsafe_methods() {
crate::on_ruby_thread(|| {
Class::new("RutieDslSplat", None).define(|klass| {
klass.def("unsafe_two", dsl_unsafe_two);
});
let product = VM::eval("RutieDslSplat.new.unsafe_two(6, 7)").unwrap();
assert_eq!(product.try_convert_to::<Fixnum>(), Ok(Fixnum::new(42)));
let product = VM::eval("RutieDslSplat.new.unsafe_two(6, 7, 8)").unwrap();
assert_eq!(product.try_convert_to::<Fixnum>(), Ok(Fixnum::new(42)));
});
}
#[test]
fn test_methods_result_drops_before_raising() {
crate::on_ruby_thread(|| {
Class::new("RutieDslResult", None).define(|klass| {
klass.def("raise", dsl_result_raise);
klass.def("double", dsl_result_double);
klass.def("unsafe_result", dsl_unsafe_result);
});
let dropped = || DROPPED.load(Ordering::SeqCst);
let before = dropped();
let result = VM::eval("RutieDslResult.new.raise(false)").unwrap();
assert!(result.is_nil());
assert_eq!(dropped(), before + 1);
let error = VM::eval("RutieDslResult.new.raise(true)").unwrap_err();
assert!(Class::from_existing("StandardError").case_equals(&error));
assert_eq!(error.message(), "kaboom");
assert_eq!(dropped(), before + 2);
let rescued = VM::eval(
"100.times.count { RutieDslResult.new.raise(true) rescue true }",
)
.unwrap();
assert_eq!(rescued.try_convert_to::<Fixnum>(), Ok(Fixnum::new(100)));
assert_eq!(dropped(), before + 102);
let error = VM::eval("RutieDslResult.new.raise(:yes)").unwrap_err();
assert!(Class::type_error().case_equals(&error));
assert_eq!(dropped(), before + 103);
let error = VM::eval("RutieDslResult.new.raise").unwrap_err();
assert!(Class::argument_error().case_equals(&error));
assert_eq!(
error.message(),
"Argument 'raise: Boolean' not found for method 'dsl_result_raise'"
);
let doubled = VM::eval("RutieDslResult.new.double(20, :a, :b)").unwrap();
assert_eq!(doubled.try_convert_to::<Fixnum>(), Ok(Fixnum::new(42)));
let error = VM::eval("RutieDslResult.new.double('20')").unwrap_err();
assert!(Class::type_error().case_equals(&error));
let quotient = VM::eval("RutieDslResult.new.unsafe_result(4)").unwrap();
assert_eq!(quotient.try_convert_to::<Fixnum>(), Ok(Fixnum::new(25)));
let before = dropped();
let error = VM::eval("RutieDslResult.new.unsafe_result(0)").unwrap_err();
assert!(Class::from_existing("ZeroDivisionError").case_equals(&error));
assert_eq!(error.message(), "divided by 0");
assert_eq!(dropped(), before + 1);
});
}
#[test]
fn test_wrappable_struct_mark_and_size() {
crate::on_ruby_thread(|| {
VM::init_loadpath();
VM::protect_require("objspace").unwrap();
let class = Class::new("RutieDslPayload", None);
let payload = Payload {
objects: vec![RString::new_utf8("kept alive").to_any_object()],
extra: 1 << 20,
};
let object: AnyObject = class.wrap_data(payload, &*PAYLOAD_WRAPPER);
GC::start();
let kept = object.get_data(&*PAYLOAD_WRAPPER).objects[0].clone();
assert_eq!(
kept.try_convert_to::<RString>().unwrap().to_str(),
"kept alive"
);
let memsize = |object: &AnyObject| {
Class::from_existing("ObjectSpace")
.protect_send("memsize_of", &[object.clone()])
.unwrap()
.try_convert_to::<Fixnum>()
.unwrap()
.to_i64()
};
assert!(memsize(&object) >= 1 << 20);
let plain: AnyObject = class.wrap_data(Plain, &*PLAIN_WRAPPER);
assert!(memsize(&plain) < 1 << 20);
});
}
}