Introduce safe function calls into Ruby

This commit is contained in:
Peter Wagenet 2019-07-30 14:03:59 -07:00
parent e8c6581203
commit 2cb2a8c6f9
12 changed files with 368 additions and 88 deletions

View File

@ -6,6 +6,9 @@ extern crate libc;
use std::ffi::CStr;
use std::mem::size_of;
#[macro_use]
mod macros;
pub const PKG_VERSION: &'static str = env!("CARGO_PKG_VERSION");
pub fn check_version() {
@ -36,6 +39,17 @@ unsafe impl Sync for ID {}
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub struct VALUE(*mut void);
impl VALUE {
pub fn wrap(ptr: *mut void) -> VALUE {
VALUE(ptr)
}
// Is this correct?
pub fn as_ptr(&self) -> *mut void {
self.0
}
}
#[repr(C)]
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub struct RubyException(isize);
@ -52,6 +66,14 @@ impl RubyException {
pub fn for_tag(tag: isize) -> RubyException {
RubyException(tag)
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn state(&self) -> isize {
self.0
}
}
pub const EMPTY_EXCEPTION: RubyException = RubyException(0);
@ -203,31 +225,25 @@ extern "C" {
#[link_name = "HELIX_T_DATA"]
pub static T_DATA: isize;
// unknown if working?
// fn rb_define_variable(name: c_string, value: *const VALUE);
// It doesn't appear that these functions will rb_raise. If it turns out they can, we
// should make sure to safe wrap them.
pub fn rb_obj_class(obj: VALUE) -> VALUE;
pub fn rb_obj_classname(obj: VALUE) -> c_string;
pub fn rb_const_get(class: VALUE, name: ID) -> VALUE;
pub fn rb_define_global_const(name: c_string, value: VALUE);
pub fn rb_define_module(name: c_string) -> VALUE;
pub fn rb_define_module_under(namespace: VALUE, name: c_string) -> VALUE;
pub fn rb_define_class(name: c_string, superclass: VALUE) -> VALUE;
pub fn rb_define_class_under(namespace: VALUE, name: c_string, superclass: VALUE) -> VALUE;
pub fn rb_define_alloc_func(class: VALUE, func: extern "C" fn(class: VALUE) -> VALUE);
pub fn rb_define_method(class: VALUE, name: c_string, func: c_func, arity: isize);
pub fn rb_define_singleton_method(class: VALUE, name: c_string, func: c_func, arity: isize);
pub fn rb_intern(string: c_string) -> ID;
pub fn rb_intern_str(string: VALUE) -> ID;
// FIXME: BEGIN: Review these for safe calls
pub fn rb_undef_method(class: VALUE, name: c_string);
pub fn rb_enc_get_index(obj: VALUE) -> isize;
pub fn rb_utf8_encindex() -> isize;
pub fn rb_sprintf(specifier: c_string, ...) -> VALUE;
pub fn rb_inspect(value: VALUE) -> VALUE;
pub fn rb_intern(string: c_string) -> ID;
pub fn rb_intern_str(string: VALUE) -> ID;
pub fn rb_sym2id(symbol: VALUE) -> ID;
pub fn rb_id2sym(id: ID) -> VALUE;
pub fn rb_id2str(id: ID) -> VALUE;
pub fn rb_ary_new() -> VALUE;
pub fn rb_ary_new_capa(capa: isize) -> VALUE;
pub fn rb_ary_new_from_values(n: isize, elts: *const VALUE) -> VALUE;
pub fn rb_ary_entry(ary: VALUE, offset: isize) -> VALUE;
pub fn rb_ary_push(ary: VALUE, item: VALUE) -> VALUE;
pub fn rb_hash_new() -> VALUE;
@ -235,35 +251,58 @@ extern "C" {
pub fn rb_hash_aset(hash: VALUE, key: VALUE, value: VALUE) -> VALUE;
pub fn rb_hash_foreach(hash: VALUE, f: extern "C" fn(key: VALUE, value: VALUE, farg: *mut void) -> st_retval, farg: *mut void);
pub fn rb_gc_mark(value: VALUE);
pub fn rb_funcall(value: VALUE, mid: ID, argc: libc::c_int, ...) -> VALUE;
pub fn rb_funcallv(value: VALUE, mid: ID, argc: libc::c_int, argv: *const VALUE) -> VALUE;
pub fn rb_scan_args(argc: libc::c_int, argv: *const VALUE, fmt: c_string, ...);
pub fn rb_funcall(value: VALUE, mid: ID, argc: isize, ...) -> VALUE;
pub fn rb_funcallv(value: VALUE, mid: ID, argc: isize, argv: *const VALUE) -> VALUE;
pub fn rb_scan_args(argc: isize, argv: *const VALUE, fmt: c_string, ...);
pub fn rb_block_given_p() -> bool;
pub fn rb_yield(value: VALUE) -> VALUE;
pub fn rb_obj_dup(value: VALUE) -> VALUE;
pub fn rb_obj_init_copy(value: VALUE, orig: VALUE) -> VALUE;
pub fn rb_raise(exc: VALUE, string: c_string, ...) -> !;
pub fn rb_jump_tag(state: RubyException) -> !;
pub fn rb_protect(try: extern "C" fn(v: *mut void) -> VALUE,
arg: *mut void,
state: *mut RubyException)
-> VALUE;
#[link_name = "HELIX_rb_str_valid_encoding_p"]
pub fn rb_str_valid_encoding_p(string: VALUE) -> bool;
#[link_name = "HELIX_rb_str_ascii_only_p"]
pub fn rb_str_ascii_only_p(string: VALUE) -> bool;
// FIXME: END: Review these for safe calls
pub fn rb_raise(exc: VALUE, string: c_string, ...) -> !;
pub fn rb_jump_tag(state: RubyException) -> !;
// In official Ruby docs, all of these *mut voids are actually VALUEs.
// However, they are interchangeable in practice and using a *mut void allows us to pass
// other things that aren't VALUEs
pub fn rb_protect(try: extern "C" fn(v: *mut void) -> *mut void,
arg: *mut void,
state: *mut RubyException)
-> *mut void;
}
// These may not all be strictly necessary. If we're concerned about performance we can
// audit and if we're sure that `rb_raise` won't be called we can avoid the safe wrapper
ruby_safe_c! {
rb_const_get(class: VALUE, name: ID) -> VALUE;
rb_define_global_const(name: c_string, value: VALUE);
rb_define_module(name: c_string) -> VALUE;
rb_define_module_under(namespace: VALUE, name: c_string) -> VALUE;
rb_define_class(name: c_string, superclass: VALUE) -> VALUE;
rb_define_class_under(namespace: VALUE, name: c_string, superclass: VALUE) -> VALUE;
rb_define_alloc_func(klass: VALUE, func: extern "C" fn(klass: VALUE) -> VALUE);
rb_define_method(class: VALUE, name: c_string, func: c_func, arity: isize);
rb_define_singleton_method(class: VALUE, name: c_string, func: c_func, arity: isize);
rb_inspect(value: VALUE) -> VALUE;
#[link_name = "HELIX_Data_Wrap_Struct"]
pub fn Data_Wrap_Struct(klass: VALUE, mark: extern "C" fn(*mut void), free: extern "C" fn(*mut void), data: *mut void) -> VALUE;
Data_Wrap_Struct(klass: VALUE, mark: extern "C" fn(*mut void), free: extern "C" fn(*mut void), data: *mut void) -> VALUE;
#[link_name = "HELIX_Data_Get_Struct_Value"]
pub fn Data_Get_Struct_Value(obj: VALUE) -> *mut void;
Data_Get_Struct_Value(obj: VALUE) -> *mut void {
fn ret_to_ptr(ret: *mut void) -> *mut void { ret }
fn ptr_to_ret(ptr: *mut void) -> *mut void { ptr }
}
#[link_name = "HELIX_Data_Set_Struct_Value"]
pub fn Data_Set_Struct_Value(obj: VALUE, data: *mut void);
Data_Set_Struct_Value(obj: VALUE, data: *mut void);
}
#[inline]

View File

@ -0,0 +1,142 @@
// TODO: See if we can simplify
#[doc(hidden)]
#[macro_export]
macro_rules! ruby_extern_fns {
// We don't need the body here
{ #[$attr:meta] $name:ident($( $argn:ident: $argt:ty ),*) -> $ret:ty { $($_body:tt)* } $($rest:tt)* } => {
ruby_extern_fns! { #[$attr] $name($( $argn: $argt ),*) -> $ret; $($rest)* } };
{ #[$attr:meta] $name:ident($( $argn:ident: $argt:ty ),*) -> $ret:ty; $($rest:tt)* } => {
#[cfg_attr(windows, link(name="helix-runtime"))]
extern "C" {
#[$attr]
pub fn $name($($argn: $argt),*) -> $ret;
}
ruby_extern_fns! { $($rest)* }
};
{ #[$attr:meta] $name:ident($( $argn:ident: $argt:ty ),*); $($rest:tt)* } => {
#[cfg_attr(windows, link(name="helix-runtime"))]
extern "C" {
#[$attr]
pub fn $name($($argn: $argt),*);
}
ruby_extern_fns! { $($rest)* }
};
// We don't need the body here
{ $name:ident($( $argn:ident: $argt:ty ),*) -> $ret:ty { $($_body:tt)* } $($rest:tt)* } => {
ruby_extern_fns! { $name($( $argn: $argt ),*) -> $ret; $($rest)* } };
{ $name:ident($( $argn:ident: $argt:ty ),*) -> $ret:ty; $($rest:tt)* } => {
#[cfg_attr(windows, link(name="helix-runtime"))]
extern "C" { pub fn $name($($argn: $argt),*) -> $ret; }
ruby_extern_fns! { $($rest)* }
};
{ $name:ident($( $argn:ident: $argt:ty ),*); $($rest:tt)* } => {
#[cfg_attr(windows, link(name="helix-runtime"))]
extern "C" { pub fn $name($($argn: $argt),*); }
ruby_extern_fns! { $($rest)* }
};
{ } => ()
}
#[doc(hidden)]
#[macro_export]
macro_rules! ruby_safe_fn {
{ $name:ident($( $argn:ident: $argt:ty ),*) -> $ret:ty { $($funcs:tt)+ } } => {
pub fn $name($( $argn: $argt ),*) -> Result<$ret, $crate::RubyException> {
// FIXME: Avoid creating args struct if there are no args
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct Args {
pub $($argn: $argt),*
};
let args = Args { $($argn: $argn),* };
// Must include ret_to_ptr and ptr_to_ret
$($funcs)+
extern "C" fn cb(args_ptr: *mut $crate::void) -> *mut $crate::void {
let ret = unsafe {
let args: &Args = &*(args_ptr as *const Args);
$crate::$name($( args.$argn ),*)
};
ret_to_ptr(ret)
}
let mut state = $crate::EMPTY_EXCEPTION;
let res = unsafe {
let args_ptr: *mut $crate::void = &args as *const _ as *mut $crate::void;
$crate::rb_protect(cb, args_ptr, &mut state)
};
if !state.is_empty() {
Err(state)
} else {
Ok(ptr_to_ret(res))
}
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! ruby_safe_fns {
// We don't need the meta here
{ #[$attr:meta] $($rest:tt)* } => {
ruby_safe_fns! { $($rest)* }
};
// It's not quite ideal to have to define each type separately, but the coercions are different
{ $name:ident($( $argn:ident: $argt:ty ),*) -> VALUE; $($rest:tt)* } => {
ruby_safe_fn! {
$name($( $argn: $argt ),*) -> $crate::VALUE {
fn ret_to_ptr(ret: $crate::VALUE) -> *mut $crate::void { ret.as_ptr() }
fn ptr_to_ret(ptr: *mut $crate::void) -> $crate::VALUE { $crate::VALUE::wrap(ptr) }
}
}
ruby_safe_fns! { $($rest)* }
};
{ $name:ident($( $argn:ident: $argt:ty ),*) -> $ret:ty { $($conv:tt)* } $($rest:tt)* } => {
ruby_safe_fn! {
$name($( $argn: $argt ),*) -> $ret { $($conv)* }
}
ruby_safe_fns! { $($rest)* }
};
{ $name:ident($( $argn:ident: $argt:ty ),*); $($rest:tt)* } => {
ruby_safe_fn! {
$name($( $argn: $argt ),*) -> () {
fn ret_to_ptr(_: ()) -> *mut $crate::void { unsafe { $crate::Qnil }.as_ptr() }
fn ptr_to_ret(_: *mut $crate::void) { }
}
}
ruby_safe_fns! { $($rest)* }
};
{ } => ()
}
#[macro_export]
macro_rules! ruby_safe_c {
{ $($parts:tt)+ } => {
ruby_extern_fns! {
$($parts)+
}
pub mod safe {
use $crate::*;
ruby_safe_fns! {
$($parts)+
}
}
}
}

View File

@ -1,2 +1,8 @@
require 'helix_runtime'
require 'console/native'
class Console
def ruby_hello
"hello"
end
end

View File

@ -65,4 +65,15 @@ describe "Console" do
expect { console.log(str) }.to raise_error(TypeError, "Expected a valid UTF-8 String, got #{str.inspect}")
end
end
it "can handle calls back to Ruby" do
expect(console.call_ruby).to eq("\"Object\", true, true")
end
it "can handle invalid calls back to Ruby" do
# NOTE: This doesn't verify that Rust unwound correctly
expect {
console.behave_badly
}.to raise_error(NameError, "undefined method `does_not_exist' for Object:Class");
end
end

View File

@ -3,7 +3,7 @@ require 'console'
module PrintMatchers
def print(expected = nil)
output(expected = nil).to_stdout_from_any_process
output(expected).to_stdout_from_any_process
end
def println(expected)

View File

@ -2,6 +2,7 @@
#[macro_use]
extern crate helix;
use helix::{sys,FromRuby};
ruby! {
#[derive(Debug)]
@ -53,5 +54,16 @@ ruby! {
def panic(&self) {
panic!("raised from Rust with `panic`");
}
def behave_badly(&self) {
ruby_funcall!(sys::rb_cObject, "does_not_exist", String::from("one"));
}
def call_ruby(&self) -> String {
let a = ruby_funcall!(sys::rb_cObject, "name"); // No arg
let b = ruby_funcall!(sys::rb_cObject, "is_a?", sys::rb_cObject); // One arg
let c = ruby_funcall!(sys::rb_cObject, "respond_to?", String::from("inspect"), true); // Two args
format!("{:?}, {:?}, {:?}", String::from_ruby_unwrap(a), bool::from_ruby_unwrap(b), bool::from_ruby_unwrap(c))
}
}
}

View File

@ -28,45 +28,39 @@ pub struct ClassDefinition {
impl ClassDefinition {
pub fn new(name: c_string) -> ClassDefinition {
let raw_class = unsafe { sys::rb_define_class(name, sys::rb_cObject) };
let raw_class = ruby_try!(sys::safe::rb_define_class(name, unsafe { sys::rb_cObject }));
ClassDefinition { class: Class(raw_class) }
}
pub fn wrapped(name: c_string, alloc_func: extern "C" fn(klass: sys::VALUE) -> sys::VALUE) -> ClassDefinition {
let raw_class = unsafe { sys::rb_define_class(name, sys::rb_cObject) };
unsafe { sys::rb_define_alloc_func(raw_class, alloc_func) };
let raw_class = ruby_try!(sys::safe::rb_define_class(name, unsafe { sys::rb_cObject }));
ruby_try!(sys::safe::rb_define_alloc_func(raw_class, alloc_func));
ClassDefinition { class: Class(raw_class) }
}
pub fn reopen(name: c_string) -> ClassDefinition {
let raw_class = unsafe {
let class_id = sys::rb_intern(name);
sys::rb_const_get(sys::rb_cObject, class_id)
};
let class_id = unsafe { sys::rb_intern(name) };
let raw_class = ruby_try!(sys::safe::rb_const_get(unsafe { sys::rb_cObject }, class_id));
ClassDefinition { class: Class(raw_class) }
}
pub fn define_method(&self, def: MethodDefinition) {
match def {
MethodDefinition::Instance(def) => {
unsafe {
sys::rb_define_method(
self.class.0,
def.name,
def.function,
def.arity
);
};
ruby_try!(sys::safe::rb_define_method(
self.class.0,
def.name,
def.function,
def.arity
));
},
MethodDefinition::Class(def) => {
unsafe {
sys::rb_define_singleton_method(
self.class.0,
def.name,
def.function,
def.arity
);
};
ruby_try!(sys::safe::rb_define_singleton_method(
self.class.0,
def.name,
def.function,
def.arity
));
}
}
}

View File

@ -12,7 +12,7 @@ mod slice;
mod vec;
mod hash;
use sys::{VALUE};
use sys::VALUE;
use super::{Error, ToError};
use std::marker::{PhantomData, Sized};

View File

@ -1,26 +1,32 @@
use super::{Class, ToRuby};
use std::{any, fmt};
use sys::{VALUE, SPRINTF_TO_S, c_string, rb_eRuntimeError, rb_raise};
use sys::{VALUE, RubyException, SPRINTF_TO_S, c_string, rb_eRuntimeError, rb_raise, rb_jump_tag};
#[derive(Copy, Clone, Debug)]
pub struct Error {
class: Class,
message: ErrorMessage
pub enum Error {
Library { class: Class, message: ErrorMessage },
Ruby(RubyException)
}
#[derive(Copy, Clone, Debug)]
enum ErrorMessage {
pub enum ErrorMessage {
Static(c_string),
Dynamic(VALUE)
}
impl Error {
// Currently unused
pub fn with_c_string(message: c_string) -> Error {
Error { class: unsafe { Class(rb_eRuntimeError) }, message: ErrorMessage::Static(message) }
Error::Library { class: unsafe { Class(rb_eRuntimeError) }, message: ErrorMessage::Static(message) }
}
pub fn with_value(message: VALUE) -> Error {
Error { class: unsafe { Class(rb_eRuntimeError) }, message: ErrorMessage::Dynamic(message) }
Error::Library { class: unsafe { Class(rb_eRuntimeError) }, message: ErrorMessage::Dynamic(message) }
}
// TODO: Can we use a trait for this?
pub fn from_ruby(exception: RubyException) -> Error {
Error::Ruby(exception)
}
pub fn from_any(any: Box<any::Any>) -> Error {
@ -32,27 +38,39 @@ impl Error {
}
pub fn with_class(self, class: Class) -> Error {
Error { class, message: self.message }
match self {
Error::Library { message, .. } => Error::Library { class, message },
_ => panic!("Only supported for Error::Library")
}
}
pub unsafe fn raise(self) -> ! {
match self.message {
ErrorMessage::Static(c_string) => rb_raise(self.class.to_value(), c_string),
ErrorMessage::Dynamic(value) => rb_raise(self.class.to_value(), SPRINTF_TO_S, value)
match self {
Error::Library { class, message } => match message {
ErrorMessage::Static(c_string) => rb_raise(class.to_value(), c_string),
ErrorMessage::Dynamic(value) => rb_raise(class.to_value(), SPRINTF_TO_S, value)
},
Error::Ruby(exception) => rb_jump_tag(exception)
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.message {
ErrorMessage::Static(c_string) => {
use ::std::ffi::CStr;
write!(f, "{}", unsafe { CStr::from_ptr(c_string) }.to_str().unwrap())
match *self {
Error::Library { message, .. } => match message {
ErrorMessage::Static(c_string) => {
use ::std::ffi::CStr;
write!(f, "{}", unsafe { CStr::from_ptr(c_string) }.to_str().unwrap())
},
ErrorMessage::Dynamic(value) => {
use super::FromRuby;
write!(f, "{}", String::from_ruby_unwrap(value))
}
},
ErrorMessage::Dynamic(value) => {
use super::FromRuby;
write!(f, "{}", String::from_ruby_unwrap(value))
Error::Ruby(_exception) => {
// FIXME: Implement properly
write!(f, "Ruby Exception")
}
}
}
@ -82,3 +100,9 @@ impl ToError for String {
Error::with_value(self.to_ruby().unwrap())
}
}
impl ToError for RubyException {
fn to_error(self) -> Error {
Error::from_ruby(self)
}
}

View File

@ -46,10 +46,12 @@ macro_rules! type_error {
};
}
#[macro_use]
mod macros;
mod class_definition;
mod coercions;
mod errors;
mod macros;
pub use coercions::*;
pub use errors::*;
@ -95,27 +97,23 @@ pub trait RubyMethod {
impl RubyMethod for extern "C" fn(VALUE) -> VALUE {
fn install(self, class: VALUE, name: &CStr) {
unsafe {
sys::rb_define_method(
class,
name.as_ptr(),
self as *const libc::c_void,
0
);
}
ruby_try!(sys::safe::rb_define_method(
class,
name.as_ptr(),
self as *const libc::c_void,
0
));
}
}
impl RubyMethod for extern "C" fn(VALUE, VALUE) -> VALUE {
fn install(self, class: VALUE, name: &CStr) {
unsafe {
sys::rb_define_method(
class,
name.as_ptr(),
self as *const libc::c_void,
1
);
}
ruby_try!(sys::safe::rb_define_method(
class,
name.as_ptr(),
self as *const libc::c_void,
1
));
}
}
@ -150,7 +148,7 @@ impl Class {
}
pub fn inspect(val: VALUE) -> String {
unsafe { String::from_ruby_unwrap(sys::rb_inspect(val)) }
String::from_ruby_unwrap(ruby_try!(sys::safe::rb_inspect(val)))
}
pub unsafe fn as_usize(value: ::VALUE) -> usize {

View File

@ -13,6 +13,9 @@ mod coercions;
#[macro_use]
mod alloc;
#[macro_use]
mod safe;
#[macro_export]
macro_rules! ruby {
{ $($rest:tt)* } => {

51
src/macros/safe.rs Normal file
View File

@ -0,0 +1,51 @@
// TODO: Can we change this to use the macro from libcruby?
#[macro_export]
macro_rules! ruby_try {
{ $val:expr } => { $val.unwrap_or_else(|e| panic!($crate::Error::from_ruby(e)) ) }
}
#[macro_export]
macro_rules! ruby_funcall {
// NOTE: Class and method cannot be variables. If that becomes necessary, I think we'll have to pass them
($rb_class:expr, $meth:expr, $( $arg:expr ),*) => {
{
use $crate::ToRuby;
// This method takes a Ruby Array of arguments
// If there is a way to make this behave like a closure, we could further simplify things.
#[allow(unused_variables)]
extern "C" fn __ruby_funcall_cb(arg_ary: *mut $crate::sys::void) -> *mut $crate::sys::void {
unsafe {
// Is this safe here?
let arg_ary = $crate::sys::VALUE::wrap(arg_ary);
// NOTE: We're using rb_intern_str, not rb_intern in the hopes that this means
// Ruby will clean up the string in the event that there is an exception
$crate::sys::rb_funcallv($rb_class, sys::rb_intern_str(String::from($meth).to_ruby().expect("valid string")),
$crate::sys::RARRAY_LEN(arg_ary), $crate::sys::RARRAY_PTR(arg_ary)).as_ptr()
}
}
let mut state = $crate::sys::EMPTY_EXCEPTION;
let res = unsafe {
let mut values_ary: Vec<$crate::sys::VALUE> = Vec::new();
$(
// We have to create this iteratively since we have to call to_ruby individually
values_ary.push($arg.to_ruby().expect("could convert to Ruby"));
)*
let ruby_values_ary = $crate::sys::rb_ary_new_from_values(values_ary.len() as isize, values_ary.as_mut_ptr());
$crate::sys::rb_protect(__ruby_funcall_cb, ruby_values_ary.as_ptr(), &mut state)
};
if !state.is_empty() {
panic!($crate::Error::from_ruby(state));
}
$crate::sys::VALUE::wrap(res)
}
};
($rb_class:expr, $meth:expr) => {
ruby_funcall!($rb_class, $meth, )
}
}