Add Symbol coercion

The main use case for symbols is to use them as a HashMap key. However,
this introduces a GC problem – we cannot store Ruby values in the heap
without properly marking/registering them, otherwise they might get
GC'ed by Ruby unexpectedly. (In fact, this is already a problem if you
have a `Vec<VALUE>`.)

I tried to avoid introducing additional problems by pinning down any
symbols that goes thought the coercion protocol. This is probably
overly aggressive, as it would cause any dynamic symbols (e.g.
`String#to_sym`) to become un-GC-able. We can revisit this once we
have a more general-purpose system to encode pinning semantics in
the type system.
This commit is contained in:
Godfrey Chan 2017-10-07 19:23:49 -07:00
parent 65a98c65d1
commit 0cdd075c22
6 changed files with 92 additions and 29 deletions

View File

@ -22,7 +22,7 @@ pub type c_string = *const libc::c_char;
// pub type c_func = extern "C" fn(...); // pub type c_func = extern "C" fn(...);
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, Debug)] #[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)]
pub struct ID(*mut void); pub struct ID(*mut void);
#[repr(C)] #[repr(C)]
@ -30,7 +30,7 @@ pub struct ID(*mut void);
pub struct VALUE(*mut void); pub struct VALUE(*mut void);
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)] #[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub struct RubyException(isize); pub struct RubyException(isize);
impl RubyException { impl RubyException {
@ -163,6 +163,9 @@ extern "C" {
#[link_name = "HELIX_T_FALSE"] #[link_name = "HELIX_T_FALSE"]
pub static T_FALSE: isize; pub static T_FALSE: isize;
#[link_name = "HELIX_T_SYMBOL"]
pub static T_SYMBOL: isize;
#[link_name = "HELIX_T_FIXNUM"] #[link_name = "HELIX_T_FIXNUM"]
pub static T_FIXNUM: isize; pub static T_FIXNUM: isize;
@ -188,6 +191,10 @@ extern "C" {
pub fn rb_sprintf(specifier: c_string, ...) -> VALUE; pub fn rb_sprintf(specifier: c_string, ...) -> VALUE;
pub fn rb_inspect(value: VALUE) -> VALUE; pub fn rb_inspect(value: VALUE) -> VALUE;
pub fn rb_intern(string: c_string) -> ID; 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_capa(capa: isize) -> VALUE; pub fn rb_ary_new_capa(capa: isize) -> VALUE;
pub fn rb_ary_entry(ary: VALUE, offset: isize) -> VALUE; pub fn rb_ary_entry(ary: VALUE, offset: isize) -> VALUE;
pub fn rb_ary_push(ary: VALUE, item: VALUE) -> VALUE; pub fn rb_ary_push(ary: VALUE, item: VALUE) -> VALUE;

View File

@ -11,13 +11,13 @@ describe "TextTransform" do
it "can widen hash" do it "can widen hash" do
expect(TextTransform.widen_hash({ expect(TextTransform.widen_hash({
"message" => "Hello", message: "Hello",
"name" => "Aaron", name: "Aaron",
"handle" => "@tenderlove" handle: "@tenderlove"
})).to eq({ })).to eq({
"" => "", "": "",
"" => "", "": "",
"" => "" "": ""
}) })
end end
@ -31,13 +31,13 @@ describe "TextTransform" do
it "can narrowen hash" do it "can narrowen hash" do
expect(TextTransform.narrowen_hash({ expect(TextTransform.narrowen_hash({
"" => "", "": "",
"" => "", "": "",
"" => "" "": ""
})).to eq({ })).to eq({
"message" => "Hello", message: "Hello",
"name" => "Aaron", name: "Aaron",
"handle" => "@tenderlove" handle: "@tenderlove"
}) })
end end
@ -51,13 +51,13 @@ describe "TextTransform" do
it "can flip hash" do it "can flip hash" do
expect(TextTransform.flip_hash({ expect(TextTransform.flip_hash({
"message" => "Hello", message: "Hello",
"name" => "Aaron", name: "Aaron",
"handle" => "@tenderlove" handle: "@tenderlove"
})).to eq({ })).to eq({
"ollǝH" => "ǝƃɐssǝɯ", "ollǝH": "ǝƃɐssǝɯ",
"uoɹɐ∀" => "ǝɯɐu", "uoɹɐ∀": "ǝɯɐu",
"ǝʌolɹǝpuǝʇ@" => "ǝlpuɐɥ" "ǝʌolɹǝpuǝʇ@": "ǝlpuɐɥ"
}) })
end end
end end

View File

@ -4,6 +4,7 @@
extern crate helix; extern crate helix;
use std::collections::HashMap; use std::collections::HashMap;
use helix::Symbol;
ruby! { ruby! {
class TextTransform { class TextTransform {
@ -30,8 +31,8 @@ ruby! {
text.into_iter().map(TextTransform::widen).collect() text.into_iter().map(TextTransform::widen).collect()
} }
def widen_hash(text: HashMap<String, String>) -> HashMap<String, String> { def widen_hash(text: HashMap<Symbol, String>) -> HashMap<Symbol, String> {
text.into_iter().map(|(k,v)| (TextTransform::widen(k), TextTransform::widen(v))).collect() text.into_iter().map(|(k,v)| (Symbol::from_string(TextTransform::widen(k.to_string())), TextTransform::widen(v))).collect()
} }
def narrowen(text: String) -> String { def narrowen(text: String) -> String {
@ -57,8 +58,8 @@ ruby! {
text.into_iter().map(TextTransform::narrowen).collect() text.into_iter().map(TextTransform::narrowen).collect()
} }
def narrowen_hash(text: HashMap<String, String>) -> HashMap<String, String> { def narrowen_hash(text: HashMap<Symbol, String>) -> HashMap<Symbol, String> {
text.into_iter().map(|(k,v)| (TextTransform::narrowen(k), TextTransform::narrowen(v))).collect() text.into_iter().map(|(k,v)| (Symbol::from_string(TextTransform::narrowen(k.to_string())), TextTransform::narrowen(v))).collect()
} }
def flip(text: String) -> String { def flip(text: String) -> String {
@ -83,8 +84,8 @@ ruby! {
text.into_iter().rev().map(TextTransform::flip).collect() text.into_iter().rev().map(TextTransform::flip).collect()
} }
def flip_hash(text: HashMap<String, String>) -> HashMap<String, String> { def flip_hash(text: HashMap<Symbol, String>) -> HashMap<Symbol, String> {
text.into_iter().map(|(k,v)| (TextTransform::flip(v), TextTransform::flip(k))).collect() text.into_iter().map(|(k,v)| (Symbol::from_string(TextTransform::flip(v)), TextTransform::flip(k.to_string()))).collect()
} }
} }
} }

View File

@ -3,6 +3,7 @@ mod unit;
mod bool; mod bool;
mod integers; mod integers;
mod float; mod float;
mod symbol;
mod string; mod string;
mod option; mod option;
mod result; mod result;

25
src/coercions/symbol.rs Normal file
View File

@ -0,0 +1,25 @@
use sys::{VALUE, RB_TYPE_P, T_SYMBOL, rb_sym2id, rb_id2sym};
use super::{FromRuby, CheckedValue, CheckResult, ToRuby, ToRubyResult};
use super::super::{Symbol};
impl FromRuby for Symbol {
type Checked = CheckedValue<Symbol>;
fn from_ruby(value: VALUE) -> CheckResult<CheckedValue<Symbol>> {
if unsafe { RB_TYPE_P(value, T_SYMBOL) } {
unsafe { Ok(CheckedValue::new(value)) }
} else {
type_error!(value, "a symbol")
}
}
fn from_checked(checked: CheckedValue<Symbol>) -> Symbol {
Symbol::from_id(unsafe { rb_sym2id(checked.to_value()) })
}
}
impl ToRuby for Symbol {
fn to_ruby(self) -> ToRubyResult {
Ok(unsafe { rb_id2sym(self.to_id()) })
}
}

View File

@ -15,7 +15,7 @@ pub extern crate libcruby_sys as sys;
// pub use rb; // pub use rb;
use std::ffi::CStr; use std::ffi::CStr;
use sys::VALUE; use sys::{VALUE, ID};
#[macro_export] #[macro_export]
macro_rules! raise { macro_rules! raise {
@ -54,10 +54,39 @@ mod macros;
pub use coercions::*; pub use coercions::*;
pub use errors::*; pub use errors::*;
#[repr(C)]
#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)]
pub struct Symbol(ID);
// Since the main use case for `Symbol` at the moment is as the
// key for a `HashMap`, this tries to avoid GC issues by alaways
// pinning the symbol, essentially making it a "copy type". This
// is probably overly aggressive, we can reconsider this when we
// have a more general-purpose mechanism to encode pinning
// semantics in the type system.
impl Symbol {
pub fn from_id(id: ID) -> Symbol {
Symbol(id)
}
pub fn to_id(self) -> ID {
self.0
}
pub fn from_string(string: String) -> Symbol {
Symbol(unsafe { sys::rb_intern_str(string.to_ruby().unwrap()) })
}
pub fn to_string(self) -> String {
unsafe { String::from_ruby_unwrap(sys::rb_id2str(self.0)) }
}
}
pub use class_definition::{ClassDefinition, MethodDefinition}; pub use class_definition::{ClassDefinition, MethodDefinition};
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, Debug)] #[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub struct Class(VALUE); pub struct Class(VALUE);
pub trait RubyMethod { pub trait RubyMethod {
@ -101,7 +130,7 @@ impl Class {
Class(value) Class(value)
} }
pub fn to_value(&self) -> VALUE { pub fn to_value(self) -> VALUE {
self.0 self.0
} }