~tim/rust-ruby-wrap-data

f5e88b97777117d08cd8e5d1f71649d703b3531b — Tim Morgan 2 years ago 9f7cbbc
Start turning this into a proper crate
6 files changed, 182 insertions(+), 132 deletions(-)

M .watchr
M Cargo.toml
M README.md
M src/lib.rs
D src/wrap.rs
D test.rb
M .watchr => .watchr +2 -11
@@ 4,18 4,9 @@ def compile
  puts
  puts '========================================='
  puts
  _, stdout, wait_thr = Open3.popen2('cargo build')
  _, stdout, wait_thr = Open3.popen2('cargo test')
  print stdout.getc until stdout.eof?
  wait_thr.value.success?
end

def test
  puts
  puts '========================================='
  puts
  _, stdout, wait_thr = Open3.popen2('ruby test.rb')
  print stdout.getc until stdout.eof?
  wait_thr.value.success?
end

watch('^src/.*|^test\.rb') { compile && test }
watch('^src/.*|^test\.rb') { compile }

M Cargo.toml => Cargo.toml +7 -5
@@ 1,11 1,13 @@
[package]
name = "rust-rbdatatype"
name = "ruby-wrap-data"
version = "0.1.0"
authors = ["Tim Morgan <tim@timmorgan.org>"]
description = "Build a Ruby object that wraps Box<T>"
homepage = "https://github.com/seven1m/rust-ruby-wrap-data"
documentation = "https://docs.rs/ruby-wrap-data"
repository = "https://github.com/seven1m/rust-ruby-wrap-data"
keywords = ["ruby"]
license = "MIT"

[dependencies]
libc = "0.2.37"
ruby-sys = "0.3.0"

[lib]
crate-type = ["dylib"]

M README.md => README.md +19 -5
@@ 1,9 1,23 @@
# Rust "C" extension that stores data inside a Ruby object
# Ruby Wrap Data

This is a bit of Rust code that demonstrates how to store any data you want inside a Ruby object
using `rb_data_object_wrap`.
`ruby_wrap_data` is a Rust crate that provides a fairly low-level means of doing
what Ruby's `Data_Wrap_Struct` macro does. That is to say, you can store a Rust
`Box<T>` inside a Ruby object and get it back out again.

Any heap-allocated struct, enum, or whatever should work.

## Testing

Assuming you're using rbenv (if not, sorry, you're on your own):

```
CONFIGURE_OPTS=--enable-shared rbenv install
RUBY=$(rbenv which ruby) cargo test
```

You may need to help Rust find the libruby.so file, like this:

```
RUBY=$(rbenv which ruby) cargo build
ruby test.rb
export LD_LIBRARY_PATH=$HOME/.rbenv/versions/2.5.1/lib
RUBY=$(rbenv which ruby) cargo test
```

M src/lib.rs => src/lib.rs +154 -48
@@ 1,60 1,166 @@
extern crate libc;
//! # Ruby Wrap Data
//!
//! `ruby_wrap_data` is a crate that provides a fairly low-level means of doing
//! what Ruby's `Data_Wrap_Struct` macro does. That is to say, you can store a
//! Rust `Box<T>` inside a Ruby object and get it back out again.
//!
//! Any heap-allocated struct, enum, or whatever should work.
//!
//! ## Example
//!
//! ```
//! extern crate ruby_sys;
//! extern crate ruby_wrap_data;
//!
//! use ruby_sys::{
//!     class::{rb_class_new_instance, rb_define_class},
//!     rb_cObject,
//!     types::Value,
//!     value::RubySpecialConsts::{Nil},
//!     vm::ruby_init
//! };
//!
//! use std::ffi::CString;
//! use std::mem;
//!
//! const RB_NIL: Value = Value { value: Nil as usize };
//!
//! struct MyValue {
//!     pub val: u16
//! }
//!
//! fn alloc(klass: Value) -> Value {
//!     // build your data and put it on the heap
//!     let data = Box::new(MyValue { val: 1 });
//!     // call `wrap()`, passing your klass and data
//!     ruby_wrap_data::wrap(klass, data)
//! }
//!
//! fn main() {
//!     // you may need to start the ruby vm
//!     unsafe { ruby_init() };
//!
//!     // create a ruby class and attach your alloc function
//!     let name = CString::new("Thing").unwrap().into_raw();
//!     let klass = unsafe { rb_define_class(name, rb_cObject) };
//!     ruby_wrap_data::define_alloc_func(klass, alloc);
//!
//!     // create a new instance of the class
//!     let thing = unsafe { rb_class_new_instance(0, &RB_NIL, klass) };
//!
//!     // get the data out of the ruby object
//!     let data: Box<MyValue> = ruby_wrap_data::get(thing);
//!     // forget the data so it's not freed
//!     mem::forget(data);
//!
//!     // set new data on the object
//!     let new_data = Box::new(MyValue { val : 2 });
//!     ruby_wrap_data::set(thing, new_data);
//! }
//! ```
extern crate ruby_sys;

mod wrap;
use ruby_sys::types::{c_void, CallbackPtr, RBasic, Value};

use ruby_sys::{class::{rb_define_class, rb_define_method}, rb_cObject, types::{CallbackPtr, Value}};

use std::ffi::CString;
use std::mem;

#[derive(Debug)]
enum MyValue {
    Vec(Vec<MyValue>),
    Str(String),
}

#[no_mangle]
pub extern "C" fn init_thing() {
    let name = CString::new("Thing").unwrap().into_raw();
    let klass = unsafe { rb_define_class(name, rb_cObject) };
    wrap::define_alloc_func(klass, alloc);
    unsafe {
        rb_define_method(
            klass,
            CString::new("show_me_the_data").unwrap().into_raw(),
            show_me_the_data as CallbackPtr,
            0,
        )
    };
    unsafe {
        rb_define_method(
            klass,
            CString::new("mutate_that_data!").unwrap().into_raw(),
            mutate_that_data as CallbackPtr,
            0,
        )
    };
extern "C" {
    pub fn rb_define_alloc_func(klass: Value, func: CallbackPtr);
    pub fn rb_data_object_wrap(
        klass: Value,
        datap: *mut c_void,
        mark: Option<extern "C" fn(*mut c_void)>,
        free: Option<extern "C" fn(*mut c_void)>,
    ) -> Value;
}

#[repr(C)]
struct RData {
    basic: RBasic,
    dmark: Option<extern "C" fn(*mut c_void)>,
    dfree: Option<extern "C" fn(*mut c_void)>,
    pub data: *mut c_void,
}

/// Defines an 'alloc' function for a Ruby class. Such a function should
/// build your initial data and call `wrap(klass, data)`.
///
/// # Arguments
///
/// * `klass` - a Ruby Class
/// * `alloc` - a function taking a Ruby Value and returning a Ruby Value
pub fn define_alloc_func(klass: Value, alloc: fn(Value) -> Value) {
    unsafe { rb_define_alloc_func(klass, alloc as CallbackPtr) };
}

pub fn wrap<T>(klass: Value, data: Box<T>) -> Value {
    let datap = Box::into_raw(data) as *mut c_void;
    unsafe { rb_data_object_wrap(klass, datap, None, Some(free::<T>)) }
}

extern "C" fn free<T>(data: *mut c_void) {
    // memory is freed when the box goes out of the scope
    let datap = data as *mut T;
    unsafe { Box::from_raw(datap) };
}

pub fn get<T>(itself: Value) -> Box<T> {
    let rdata = rdata(itself);
    let datap = unsafe { (*rdata).data as *mut T };
    unsafe { Box::from_raw(datap) }
}

fn alloc(klass: Value) -> Value {
    let data: Box<Vec<MyValue>> = Box::new(vec![]);
    wrap::wrap(klass, data)
pub fn set<T>(itself: Value, data: Box<T>) {
    let rdata = rdata(itself);
    let datap = Box::into_raw(data) as *mut c_void;
    unsafe { (*rdata).data = datap };
}

fn show_me_the_data(itself: Value) -> Value {
    let data: Box<Vec<MyValue>> = wrap::get(itself);
    println!("{:?}", data);
    mem::forget(data); // don't free this memory
    itself
fn rdata(object: Value) -> *mut RData {
    unsafe { mem::transmute(object) }
}

fn mutate_that_data(itself: Value) -> Value {
    println!("mutating the data");
    let new_data: Box<Vec<MyValue>> = Box::new(vec![
        MyValue::Str("foo".to_string()),
        MyValue::Vec(vec![MyValue::Str("bar".to_string())]),
    ]);
    wrap::set(itself, new_data);
    itself
#[cfg(test)]
mod tests {
    use super::*;

    use ruby_sys::{
        class::{rb_class_new_instance, rb_define_class},
        rb_cObject,
        types::Value,
        value::RubySpecialConsts::{Nil},
        vm::ruby_init
    };

    use std::ffi::CString;
    use std::mem;

    const RB_NIL: Value = Value { value: Nil as usize };

    #[derive(Debug,PartialEq)]
    struct MyValue {
        pub val: u16
    }

    fn alloc(klass: Value) -> Value {
        let data = Box::new(MyValue { val: 1 });
        wrap(klass, data)
    }

    #[test]
    fn it_works() {
        unsafe { ruby_init() };
        let name = CString::new("Thing").unwrap().into_raw();
        let klass = unsafe { rb_define_class(name, rb_cObject) };
        define_alloc_func(klass, alloc);
        let thing = unsafe { rb_class_new_instance(0, &RB_NIL, klass) };
        let data: Box<MyValue> = get(thing);
        assert_eq!(*data, MyValue { val: 1 });
        mem::forget(data);
        let new_data = Box::new(MyValue { val : 2 });
        set(thing, new_data);
        let data: Box<MyValue> = get(thing);
        assert_eq!(*data, MyValue { val: 2 });
        mem::forget(data);
    }
}

D src/wrap.rs => src/wrap.rs +0 -52
@@ 1,52 0,0 @@
use ruby_sys::types::{c_void, CallbackPtr, RBasic, Value};

use std::mem;

extern "C" {
    pub fn rb_define_alloc_func(klass: Value, func: CallbackPtr);
    pub fn rb_data_object_wrap(
        klass: Value,
        datap: *mut c_void,
        mark: Option<extern "C" fn(*mut c_void)>,
        free: Option<extern "C" fn(*mut c_void)>,
    ) -> Value;
}

#[repr(C)]
struct RData {
    basic: RBasic,
    dmark: Option<extern "C" fn(*mut c_void)>,
    dfree: Option<extern "C" fn(*mut c_void)>,
    pub data: *mut c_void,
}

pub fn define_alloc_func(klass: Value, alloc: fn(Value) -> Value) {
    unsafe { rb_define_alloc_func(klass, alloc as CallbackPtr) };
}

pub fn wrap<T>(klass: Value, data: Box<T>) -> Value {
    let datap = Box::into_raw(data) as *mut c_void;
    unsafe { rb_data_object_wrap(klass, datap, None, Some(free::<T>)) }
}

extern "C" fn free<T>(data: *mut c_void) {
    // memory is freed when the box goes out of the scope
    let datap = data as *mut T;
    unsafe { Box::from_raw(datap) };
}

pub fn get<T>(itself: Value) -> Box<T> {
    let rdata = rdata(itself);
    let datap = unsafe { (*rdata).data as *mut T };
    unsafe { Box::from_raw(datap) }
}

pub fn set<T>(itself: Value, data: Box<T>) {
    let rdata = rdata(itself);
    let datap = Box::into_raw(data) as *mut c_void;
    unsafe { (*rdata).data = datap };
}

fn rdata(object: Value) -> *mut RData {
    unsafe { mem::transmute(object) }
}

D test.rb => test.rb +0 -11
@@ 1,11 0,0 @@
require 'fiddle'

library = Fiddle::dlopen("target/debug/librust_rbdatatype.so")
init_thing = Fiddle::Function.new(library['init_thing'], [], Fiddle::TYPE_VOID)
init_thing.call

thing = Thing.new
p thing
thing.show_me_the_data
thing.mutate_that_data!
thing.show_me_the_data