Skip to content

Handling references to Ruby values

Philipp Schulz edited this page May 18, 2022 · 2 revisions

Anyolite allows not only for handling Crystal values from within Ruby, but also Ruby values from within Crystal. This can be used, for example, to build containers of Ruby values or to call instance methods on them for scripting purposes.

To avoid memory leaks, Ruby values are generally handled using the Anyolite::RbRef struct, which makes sure that the Ruby object stays alive as long as it is alive in Crystal.

An easy way to access these references is by using them as function arguments:

module Test
  def self.check_for_string(ref : Anyolite::RbRef)
    ref.is_a_string?
  end
end

In this code, ref translates to an arbitrary Ruby object and thus the function accepts everything. The is_a_string? method then checks whether the value is a Ruby String value and returns either true or false.

It is also possible to test for arbitrary other classes:

class TestClass
end

module Test
  def check_for_test_class(ref : Anyolite::RbRef)
    ref.is_a_custom?(TestClass)
  end
end

This way, references can be safely checked for their types without throwing any exceptions. If you want to actually cast a value, you can use the following approach (which returns nil if the given value is not a TestClass instance:

class TestClass
end

module Test
  def self.convert_to_test_class(ref : Anyolite::RbRef)
    if ref.is_a_custom?(TestClass)
      Anyolite.cast_to_crystal(ref, TestClass)
    else
      nil
    end
  end
end

You can also call instance methods of the passed objects:

class TestClass
  def greet(name : String)
    "Hello world, #{name}!"
  end
end

module Test
  def self.call_method_for_test_class(ref : Anyolite::RbRef, method_name : String)
    Anyolite.call_rb_method_of_object(ref, method_name, ["Some guy"], cast_to: String)
  end
end

Then, the following Ruby code will result in the output "Hello world, Some guy!":

tc = TestClass.new
puts Test.call_method_for_test_class(ref: tc, method_name: "greet")

The relevant arguments for Anyolite.call_rb_method_of_object here are an RbRef object (alternatively, a Crystal object can be passed as well, in which case a temporary Ruby object will be created instead), the method name (as Symbol or String variable), an Array with the arguments (can be Crystal values or RbRef values) or nil for no arguments and the optional cast_to keyword argument, which specifies the type of the returned value (if nothing was given, it will become a new RbRef instance).

Furthermore, you can even access instance variables:

class Person
  attr_accessor :name
end

module Test
  def self.get_name_of_person(ref : Anyolite::RbRef)
    if ref.is_a_custom?(Person)
      Anyolite.get_iv(ref, :name, cast_to: String)
    else
      nil
    end
  end

  def self.set_name_of_person(ref : Anyolite::RbRef, new_name : String)
    if ref.is_a_custom?(Person)
      Anyolite.set_iv(ref, :name, new_name)
    end
  end
end

Returning RbRef values is also completely valid and will simply pass their content directly to Ruby. Only if the reference to the object is removed from both Crystal and Ruby, the object will be actually deleted, so passing RbRef between Crystal and Ruby is completely safe.