Embed Crystal code directly in Ruby
crystalruby
crystalruby is a gem that allows you to write Crystal code, inlined in Ruby.
All you need is a modern crystal compiler installed on your system.
You can then turn simple methods into Crystal methods as easily as demonstrated below:
require 'crystalruby'
# The below method will be replaced by a compiled Crystal version
# linked using FFI.
crystallize
def add(a: Int32, b: Int32, returns: Int32)
a + b
end
# This method is run in Crystal, not Ruby!
puts add(1, 2) # => 3With as small a change as this, you should be able to see a significant increase in performance for several classes of CPU or memory intensive code. E.g.
…3.04239400010556 # Ruby
0.06029000016860 # Crystal (50x faster)Note: The first, unprimed run of the Crystal code will be slower, as it needs to compile the code first. The subsequent runs will be much faster.
You can call embedded crystal code, from within other embedded crystal code.
The below crystallized method redis_set_and_return calls the redis_get method, which is also crystallized.
Note the use of the shard command to define the Redis shard dependency of the crystallized code.
E.g.
require 'crystalruby'
module Cache
shard :redis, github: 'jgaskins/redis'
crystallize :string
def redis_get(key: String)
rds = Redis::Client.new
value = rds.get(key).to_s
end
crystallize :string
def redis_set_and_return(key: String, value: String)
redis = Redis::Client.new
redis.set(key, value)
Cache.redis_get(key)
end
end
Cache.redis_set_and_return('test', 'abc')
puts Cache.redis_get('test')$ abcTo define a method that will be compiled as Crystal, you can use the crystallize method.
You must also provide types, for the parameters and return type.
Parameter types are defined using kwarg syntax, with the type as the value. E.g.
def foo(a: Int32, b: Array(Int), c: String)Return types are specified using either a lambda, returning the type, as the first argument to the crystallize method, or the special returns kwarg.
E.g.
# Returns an Int32
crystallize ->{ Int32 }
def returns_int32
3
end
# You can use the symbol shortcode for primitive types
crystallize :int32
def returns_int32
3
end
# Define the return type directly using the `returns` kwarg
crystallize
def returns_int32(returns: Int32)
3
endWhere the Crystal syntax of the method body is also valid Ruby syntax, you can just write Ruby. It'll be compiled as Crystal automatically.
E.g.
crystallize :int
def add(a: :int, b: :int)
puts "Adding #{a} and #{b}"
a + b
endSome Crystal syntax is not valid Ruby, for methods of this form, we need to
define our functions using the raw: true option
crystallize raw: true
def add(a: :int, b: :int)
:return_type
def foo(arg1, arg2)In crystalruby > 0.3.x, argument types are now passed as keyword arguments, and the return type is passed either as a keyword argument or as the first argument to crystallize (either using symbol shorthand, or a Lambda returning a Crystal type).
# V >= 0.3.x
crystallize :return_type
def foo(arg1: :arg1_type, arg2: :arg2_type)
# OR use the `returns` kwarg
crystallize
def foo(arg1: :arg1_type, arg2: :arg2_type, returns: :return_type)The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.
# crystalrubytest.rb
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'crystalruby'
end
require 'crystalruby'
crystallize :int
def add(a: :int, b: :int)
a + b
end
puts add(1, 2)Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.
Primitive types short-hands
For composite and union types, you can declare these within the function signature, using a syntax similar to Crystal's type syntax.
E.g.
require 'crystalruby'
crystallize
def complex_argument_types(a: Int64 | Float64 | Nil, b: String | Array(Bool))
puts "Got #{a} and #{b}"
end
crystallize
def complex_return_type(returns: Int32 | String | Hash(String, Array(NamedTuple(hello: Int32)) | Time))
return {
"hello" => [
{
hello: 1,
},
],
"world" => Time.utc
}
end
complex_argument_types(10, "Hello")
puts complex_return_type()Type signatures validations are applied to both arguments and return types.
[1] pry(main)> complex_argument_types(nil, "test")
Got and test
=> nil
[2] pry(main)> complex_argument_types(88, [true, false, true])
Got 88 and [true, false, true]
=> nil
[3] pry(main)> complex_argument_types(88, [true, false, 88])
ArgumentError: Expected Bool but was Int at line 1, column 15
from crystalruby.rb:303:in `block in compile!'By default, all types are passed by value, as there is an implicit copy each time a value is passed between Crystal and Ruby. However, if you name a type you can instantiate it (in either Ruby or Crystal) and pass by reference instead. This allows for more efficient passing of large data structures between the two languages.
crystalruby implements a shared reference counter, so that the same object can be safely used across both languages
without fear of them being garbage collected prematurely.
E.g.
IntArrOrBoolArr = CRType{ Array(Bool) | Array(Int32) }
crystallize
def method_with_named_types(a: IntArrOrBoolArr, returns: IntArrOrBoolArr)
return a
end
# In this case the array is converted to a Crystal Array (so a copy is made)
method_with_named_types([1,2,3])
# In this case, no conversion is necessary and the array is passed by reference
int_array = IntArrOrBoolArr.new([1,2,3]) # Or IntArrOrBoolArr[1,2,3]
method_with_named_types(int_array)We can demonstrate the significant performance advantage of passing by reference with the following benchmark.
require 'benchmark'
require 'crystalruby'
IntArray = CRType{ Array(Int32) }
crystallize
def array_doubler(a: IntArray)
a.map! { |x| x * 2 }
end
def array_doubler_rb(a)
a.map! { |x| x * 2 }
end
big_array = Array.new(1_000_000) { rand(100) }
big_int_array = IntArray.new(big_array)
Benchmark.bm do |x|
x.report("Crystal Pass by value") { array_doubler(big_array) }
x.report("Crystal Pass by reference") { array_doubler(big_int_array) }
x.report("Ruby Pass by reference") { array_doubler_rb(big_array) }
endYou can even define instance methods on an instance of a reference type, to make addressable objects that are shared between Ruby and Crystal.
require 'crystalruby'
class Person 30
invoke_yielder_rb(50, 50) #=> 100Exceptions thrown in Crystal code can be caught in Ruby.
You can specify shard dependencies inline in your Ruby source, using the shard method.
shard :redis, github: 'stefanwille/crystal-redis'
Any options you pass to the shard method will be added to the corresponding shard dependency in the autogenerated shard.yml file.
crystalruby will automatically
shards install for youIf your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.
Sometimes you may want to wrap a Crystal method in Ruby, so that you can use Ruby before the Crystal code to prepare arguments, or after the Crystal code, to apply transformations to the result. A real-life example of this might be an ActionController method, where you might want to use Ruby to parse the request, perform auth etc., and then use Crystal to perform some heavy computation, before returning the result from Ruby.
To do this, you simply pass a block to the crystallize method, which will serve as the Ruby entry point to the function. From within this block, you can invoke super to call the Crystal method, and then apply any Ruby transformations to the result.
crystallize :int32 do |a, b|
# In this example, we perform automated conversion to integers inside Ruby.
# Then add 1 to the result of the Crystal method.
result = super(a.to_i, b.to_i)
result + 1
end
def convert_to_i_and_add_and_succ(a: :int32, b: :int32)
a + b
end
puts convert_to_i_and_add_and_succ("1", "2")crystalruby also allows you to write top-level Crystal code outside of method definitions. This can be useful for e.g. performing setup operations or initializations.
Follow these steps for a toy example of how we can use crystallized ruby and inline chunks to expose the crystal-redis library to Ruby.
mkdir crystalredis
cd crystalredis
bundle initbundle install# frozen_string_literal: true
source "https://rubygems.org"
gem 'crystalruby'
# Let's see if performance is comparable to that of the redis gem.
gem 'benchmark-ips'
gem 'redis'# Filename: crystalredis.rb
require 'crystalruby'
module CrystalRedis
shard :redis, github: 'stefanwille/crystal-redis'
crystal do
CLIENT = Redis.new
def self.client
CLIENT
end
end
crystallize
def set(key: String, value: String)
client.set(key, value)
end
crystallize :string
def get(key: String)
client.get(key).to_s
end
end…$ bundle exec ruby benchmark.rbYou can control whether crystalruby builds in debug or release mode by setting following config option
CrystalRuby.configure do |config|
config.debug = false
endBy default, Crystal code is only JIT compiled. In production, you likely want to compile the Crystal code ahead of time. To do this, you can create a dedicated file which
E.g.
# E.g. crystalruby_build.rb
require "crystalruby"
CrystalRuby.configure do |config|
config.debug = false
end
require_relative "foo"
require_relative "bar"
CrystalRuby.compile!Then you can run this file as part of your build step, to ensure all Crystal code is compiled ahead of time.
While Ruby programs allow multi-threading, Crystal (if not using experimental multi-thread support) uses only a single thread and utilises Fiber based cooperative-multitasking to allow for concurrent execution. This means that by default, Crystal libraries can not safely be invoked in parallel across multiple Ruby threads.
To safely utilise crystalruby in a multithreaded environment, crystalruby implements a Reactor, which multiplexes all Ruby calls to Crystal across a single thread.
By default crystalruby methods are blocking/synchronous, this means that for blocking operations, a single crystalruby call can block the entire reactor across all threads.
To allow you to benefit from Crystal's fiber based concurrency, you can use the async: true option on crystallized ruby methods. This allows several Ruby threads to invoke Crystal code simultaneously.
E.g.
module Sleeper
crystallize
def sleep_sync
sleep 2.seconds
end
crystallize async: true
def sleep_asynNo open issues yet, or sync has not completed.