Skip to content

Instantly share code, notes, and snippets.

@spalladino
Created November 23, 2016 15:40
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save spalladino/10c829db3191a89a8ba73bb001d3c921 to your computer and use it in GitHub Desktop.
Save spalladino/10c829db3191a89a8ba73bb001d3c921 to your computer and use it in GitHub Desktop.
Proof of concept of Ruby extension written in Crystal

Sample Ruby extension in Crystal

Tested on Crystal 0.19.4 and Ruby 2.3.1, on OSX El Capitan.

Run with make irb:

crystal-ruby make irb
irb -rtestruby -I.
irb(main):001:0> coco = Coco.new("3")
=> #<Coco:0x007ff0b3039840>
irb(main):002:0> coco.size
=> 3
irb(main):003:0> get "https://raw.githubusercontent.com/manastech/crystal_ruby/master/README.md"
=> "crystal_ruby\n============\n\nWrite Ruby extensions in Crystal. This is just a [Proof of Concept](http://en.wikipedia.org/wiki/Proof_of_concept).\n"

Based on https://github.com/manastech/crystal_ruby, thanks to @waj, @asterite and @mverzilli for the original work and the work on this PoC.

Caveats

This is just a PoC, that could be used to derive a friendlier API to writing Ruby extensions, so use it at your own risk. There is much work to do yet, and all invocations to Crystal code from a Ruby Thread other than the main one cause a SEGFAULT.

testruby.bundle: testruby.cr
crystal testruby.cr --link-flags "-dynamic -bundle -Wl,-undefined,dynamic_lookup" -o testruby.bundle
irb: testruby.bundle
irb -rtestruby -I.
clean:
rm -rf .crystal testruby.bundle
require "http/client"
lib LibRuby
type VALUE = Void*
type ID = Void*
type RUBY_DATA_FUNC = Void* -> Void
$rb_cObject : VALUE
struct RBasic
flags : VALUE
klass : VALUE
end
struct RData
basic : RBasic
mark : RUBY_DATA_FUNC
free : RUBY_DATA_FUNC
data : Void*
end
fun rb_define_global_function(name : UInt8*, f : Void*, args : Int32)
fun rb_eval_string(str : UInt8*) : VALUE
fun rb_str_new_cstr(str : UInt8*) : VALUE
fun rb_define_class(name : UInt8*, parent : VALUE) : VALUE
fun rb_define_method(clazz : VALUE, name : UInt8*, f : Void*, args : Int32)
fun rb_any_to_s(v : VALUE) : VALUE
fun rb_string_value_cstr(v : VALUE*) : UInt8*
fun rb_intern(name : UInt8*) : ID
fun rb_funcall(obj : VALUE, func : ID, args : Int32, ...) : VALUE
fun rb_iv_set(obj : VALUE, name : UInt8*, value : VALUE) : VALUE
fun rb_iv_get(obj : VALUE, name : UInt8*) : VALUE
fun rb_data_object_wrap(klass : VALUE, ptr : Void*, mark : RUBY_DATA_FUNC, free : RUBY_DATA_FUNC) : VALUE
fun rb_define_alloc_func(klass : VALUE, alloc : VALUE -> VALUE)
end
struct Int
def to_ruby
Pointer(Void).new((self << 1) | 1).as(LibRuby::VALUE)
end
end
struct Nil
def to_ruby
Pointer(Void).new(8_u64).as(LibRuby::VALUE)
end
end
struct Bool
def to_ruby
Pointer(Void).new(self ? 20_u64 : 0_u64).as(LibRuby::VALUE)
end
end
class String
def to_ruby
LibRuby.rb_str_new_cstr(self)
end
def self.from_ruby(str : LibRuby::VALUE)
Ruby::Value.new(str).to_s
end
end
module Ruby
ID_TO_S = LibRuby.rb_intern("to_s")
struct Class
def initialize(name)
@class = LibRuby.rb_define_class(name, LibRuby.rb_cObject)
end
def allocate(f)
LibRuby.rb_define_alloc_func(@class, f)
end
def def(name, args, f)
LibRuby.rb_define_method(@class, name, f.pointer, args)
end
def to_unsafe
@class
end
def to_ruby
@class
end
end
struct Value
@value : LibRuby::VALUE
def initialize(@value : LibRuby::VALUE)
end
def to_unsafe
@value
end
def to_s
str = LibRuby.rb_funcall(self, ID_TO_S, 0)
String.new(LibRuby.rb_string_value_cstr(pointerof(str)))
end
end
def self.global_def(name, args, f)
LibRuby.rb_define_global_function(name, f.pointer, args)
end
end
# Keep references to a set of objects sent to Ruby, to prevent them from being GC'ed
Roots = Set(Void*).new
# Generic wrapper for sending Crystal objects to Ruby
CrystalObject = Ruby::Class.new("CrystalObject")
Ruby::Class.new("Foo").tap do |c|
c.def "bar", 1, ->(self : LibRuby::VALUE, a : LibRuby::VALUE) do
a = Ruby::Value.new(a)
"From Crystal!! #{a.to_s}".to_ruby
end
end
# Methods for wrapping/unwrapping Crystal references as Ruby Values
class Reference
def wrap(t = CrystalObject)
LibRuby.rb_data_object_wrap(t, self.as(Void*), nil, ->crystal_object_free)
end
def self.unwrap(value : LibRuby::VALUE)
value.as(LibRuby::RData*).value.data.as(self)
end
def wrap(value : LibRuby::VALUE)
value.as(LibRuby::RData*).value.data = self.as(Void*)
end
end
# When Ruby signals that an object can be GC'ed, removed it from Roots
fun crystal_object_free(obj : Void*)
Roots.delete obj
end
# Sample Crystal class to be wrapped as a Ruby class
class Coco
@data = {"foo" => "1", "bar" => "2"}
def initialize(x)
@data["baz"] = x
end
def size
@data.size
end
end
Ruby::Class.new("Coco").tap do |c|
c.allocate ->(c : LibRuby::VALUE) do
# We send a nil reference to data_object_wrap, which we will overwite later once we initialize the object
# Alternatively, we could call `Coco.allocate` here, wrap it, and call the `initialize` method on the Ruby initialize
LibRuby.rb_data_object_wrap(c, nil, nil, ->crystal_object_free)
end
c.def "initialize", 1, ->(self : LibRuby::VALUE, x : LibRuby::VALUE) do
# Do create an instance of Coco here, using the args provided, wrap it and return it
# Alternatively, we could run `Coco.unwrap(self).initialize("lala")` if we had allocated it in the Ruby `allocate method`
arg = String.from_ruby(x)
coco = Coco.new(arg)
Roots << coco.as(Void*)
coco.wrap(self)
end
c.def "size", 0, ->(self : LibRuby::VALUE) do
# Delegate a simple method to Coco, and wrap the return value as a Ruby value
Coco.unwrap(self).size.to_ruby
end
end
# Sample of a global method with an argument that uses Fibers
Ruby.global_def "get", 1, ->(obj : LibRuby::VALUE, rburl : LibRuby::VALUE) do
begin
url = String.from_ruby(rburl)
HTTP::Client.get(url).body.to_ruby
rescue ex
# We should be wrapping the Crystal exception in a Ruby via `rb_raise`
puts "Exception in get: #{ex}"
"".to_ruby
end
end
# Wrap Crystal main
fun init = "Init_testruby"(argc : Int32, argv : UInt8**)
GC.init
begin
LibCrystalMain.__crystal_main(argc, argv)
rescue ex
ex.inspect_with_backtrace STDERR
end
end
@mverzilli
Copy link

Suggestion: change so use it at your own risk. with Use it at your own risk, but do let us know what you use it for and what problems you stumble upon..

We should probably either add this to crystal-ruby or start a new repo to work seriously on this :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment