~tim/scheme-vm

8d46d40685bed348677f651e5f80c86098fecad3 — Tim Morgan 3 years ago b7d9995
I finally figured it out... It was GC!!!

I mistakenly assumed that Rust was my enemy here... freeing memory that
it no longer "saw", e.g. values passed back to Ruby. In reality, it was
Ruby garbage collecting memory for objects that hadn't yet been stored
in a variable.

One thing that kept me from considering GC as a suspect was that I
assumed that, while in "C" code, Ruby's garbage collector would not
attempt to free memory. This was wrong!

Ruby's GC will run any time Ruby attempts to allocate new memory and
decides it has already allocated a bunch of objects -- it runs a GC to
possibly free up existing objects rather than just consume more memory.
I had assumed that since I was in Rust (er.. "C" extension code), the GC
would wait until control returned to Ruby. Well, duh! Any time my Rust
code called a `rb_*` function, *that's* Ruby code!

I read about lots of tricks for telling Ruby not to GC objects created
from within a C extension, including marking objects as global
variables, registering them with GC, etc. In the end, I decided it was
best to just disable GC before building the AST and re-enabling it just
before passing the AST back to Ruby-land.

Whew!

Funny thing is: this bug took me a full year to understand! I've been
toying with this bug off-and-on since Feb 13, 2017! Yay for persistence!

:-D
3 files changed, 18 insertions(+), 24 deletions(-)

M parser.rb
M src/lib.rs
M src/rb.rs
M parser.rb => parser.rb +0 -13
@@ 20,21 20,8 @@ class Parser
  end

  def parse
    # FIXME: there is a use-after-free error somewhere in my Rust code.
    # I'm deep duping the AST until I can learn how to valgrind. :-)
    #deep_dup(parse_native)
    parse_native
  end

  private

  def deep_dup(ary)
    if ary.respond_to?(:map)
      ary.map { |i| deep_dup(i) }
    else
      ary.dup
    end
  end
end

library = Fiddle::dlopen('target/release/libscheme_vm.dylib')

M src/lib.rs => src/lib.rs +6 -1
@@ 15,9 15,14 @@ mod lisp {

fn parse_native(rself: Value) -> Value {
    let program_str = rbstr2str!(&rb::ivar_get(&rself, "@code"));
    rb::gc_disable();
    match lisp::program(&program_str) {
        Ok(ast) => ast,
        Ok(ast) => {
            rb::gc_enable();
            ast
        },
        Err(err) => {
            rb::gc_enable();
            //let expected = rb::vec2rbarr(
                //err.expected.iter().cloned().map(|e| rb::str_new(&e.to_string())).collect()
            //);

M src/rb.rs => src/rb.rs +12 -10
@@ 1,6 1,5 @@
// borrowed from https://github.com/ubcsanskrit/sanscript.rb (MIT)

use std::mem;
use ruby_sys::{class, array, string};
use ruby_sys::types::{c_char, c_int, c_long};
use ruby_sys::util::{rb_const_get};


@@ 40,6 39,7 @@ macro_rules! str2rbid {
    ($s:expr) => { ::ruby_sys::util::rb_intern(str2cstrp!($s)) }
}

#[allow(unused_macros)]
macro_rules! str2sym {
    ($s:expr) => {
        unsafe { ::ruby_sys::symbol::rb_id2sym(str2rbid!($s)) }


@@ 80,7 80,6 @@ pub fn define_class_under(outer: &Value, name: &str, superclass: &Value) -> Valu
pub fn class_new_instance(klass: &Value, args: Vec<Value>) -> Value {
    let argc = args.len() as c_int;
    let instance = unsafe { class::rb_class_new_instance(argc, args.as_ptr(), *klass) };
    mem::forget(args);
    instance
}



@@ 100,13 99,11 @@ pub fn define_module_under(parent: &Value, name: &str) -> Value {

pub fn define_method(module: &Value, name: &str, method: CallbackPtr, argc: i32) {
    unsafe { class::rb_define_method(*module, str2cstrp!(name), method, argc) }
    mem::forget(method);
}

#[allow(dead_code)]
pub fn define_singleton_method(module: &Value, name: &str, method: CallbackPtr, argc: i32) {
    unsafe { class::rb_define_singleton_method(*module, str2cstrp!(name), method, argc) }
    mem::forget(method);
}

#[allow(dead_code)]


@@ 120,24 117,19 @@ pub fn ary_new() -> Value {

pub fn ary_push(array: Value, item: Value) -> Value {
    let new_array = unsafe { array::rb_ary_push(array, item) };
    mem::forget(array);
    mem::forget(item);
    new_array
}

pub fn str_new(string: &String) -> Value {
    let str = string.as_ptr() as *const c_char;
    let len = string.len() as c_long;
    let rb_str = unsafe { string::rb_str_new(str, len) };
    mem::forget(string);
    rb_str
    unsafe { string::rb_str_new(str, len) }
}

pub fn vec2rbarr(vec: Vec<Value>) -> Value {
    let mut arr = ary_new();
    for item in vec {
        arr = ary_push(arr, item);
        mem::forget(item);
    }
    arr
}


@@ 153,6 145,8 @@ pub fn const_get(name: &str, class: &Value) -> Value {

extern "C" {
    pub fn rb_exc_raise(object: Value);
    pub fn rb_gc_enable();
    pub fn rb_gc_disable();
}

#[allow(dead_code)]


@@ 163,3 157,11 @@ pub fn raise(exception: &Value, message: &str) {
pub fn raise_instance(object: &Value) {
    unsafe { rb_exc_raise(*object) };
}

pub fn gc_enable() {
    unsafe { rb_gc_enable() };
}

pub fn gc_disable() {
    unsafe { rb_gc_disable() };
}