Repository files navigation

logo

crystalruby

GEM Version
GEM Downloads

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.crystallizedefadd(a: Int32,b: Int32,returns: Int32)a + bend# This method is run in Crystal, not Ruby!putsadd(1,2)# => 3

With 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.

require'crystalruby'require'benchmark'crystallize:int32defcount_primes_upto_cr(n: Int32)(2..n).each.countdo |i|
is_prime=true(2..Math.isqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendenddefcount_primes_upto_rb(n)(2..n).each.countdo |i|
is_prime=true(2..Integer.sqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendendputsBenchmark.realtime{count_primes_upto_rb(1_000_000)}putsBenchmark.realtime{count_primes_upto_cr(1_000_000)}
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'moduleCacheshard:redis,github: 'jgaskins/redis'crystallize:stringdefredis_get(key: String)rds=Redis::Client.newvalue=rds.get(key).to_sendcrystallize:stringdefredis_set_and_return(key: String,value: String)redis=Redis::Client.newredis.set(key,value)Cache.redis_get(key)endendCache.redis_set_and_return('test','abc')putsCache.redis_get('test')
$ abc

Syntax

To 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.

Method Signatures

Parameter types are defined using kwarg syntax, with the type as the value. E.g.

deffoo(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 Int32crystallize->{Int32}defreturns_int323end# You can use the symbol shortcode for primitive typescrystallize:int32defreturns_int323end# Define the return type directly using the `returns` kwargcrystallizedefreturns_int32(returns: Int32)3end

Ruby Compatible Method Bodies

Where 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:intdefadd(a: :int,b: :int)puts"Adding #{a} and #{b}"a + bend

Crystal-only Syntax

Some Crystal syntax is not valid Ruby, for methods of this form, we need to define our functions using the raw: true option

crystallizeraw: truedefadd(a: :int,b: :int)<<~CRYSTAL c = 0_u64 a + b + c CRYSTALend

Upgrading from version 0.2.x

Change in type signatures

In version 0.2.x, argument and return types were passed to the crystallize method using a different syntax:

# V <= 0.2.xcrystallize[arg1: :arg1_type,arg2: :arg2_type]=>:return_typedeffoo(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.xcrystallize:return_typedeffoo(arg1: :arg1_type,arg2: :arg2_type)# OR use the `returns` kwargcrystallizedeffoo(arg1: :arg1_type,arg2: :arg2_type,returns: :return_type)

Getting Started

The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.

# crystalrubytest.rbrequire'bundler/inline'gemfiledosource'https://rubygems.org'gem'crystalruby'endrequire'crystalruby'crystallize:intdefadd(a: :int,b: :int)a + bendputsadd(1,2)

Types

Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.

Supported Types

  • UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
  • Time
  • Symbol
  • Nil
  • Bool
  • Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
  • Proc

Primitive types short-hands

  • :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
  • :int :uint :int32 :uint32 :long :ulong :int64 :uint64
  • :long_long :ulong_long :float :double :bool
  • :void :pointer :string

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'crystallizedefcomplex_argument_types(a: Int64 | Float64 | Nil,b: String | Array(Bool))puts"Got #{a} and #{b}"endcrystallizedefcomplex_return_type(returns: Int32 | String | Hash(String,Array(NamedTuple(hello: Int32)) | Time))return{"hello"=>[{hello: 1,},],"world"=>Time.utc}endcomplex_argument_types(10,"Hello")putscomplex_return_type()

Type signatures validations are applied to both arguments and return types.

[1]pry(main)> complex_argument_types(nil,"test")Gotandtest=>nil[2]pry(main)> complex_argument_types(88,[true,false,true])Got88and[true,false,true]=>nil[3]pry(main)> complex_argument_types(88,[true,false,88])ArgumentError: ExpectedBoolbutwasIntatline1,column15fromcrystalruby.rb:303:in `blockin compile!'

Reference Types

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)}crystallizedefmethod_with_named_types(a: IntArrOrBoolArr,returns: IntArrOrBoolArr)returnaend# 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 referenceint_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)}crystallizedefarray_doubler(a: IntArray)a.map!{ |x| x * 2}enddefarray_doubler_rb(a)a.map!{ |x| x * 2}endbig_array=Array.new(1_000_000){rand(100)}big_int_array=IntArray.new(big_array)Benchmark.bmdo |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)}end

Shared Instances

You 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'classPerson < CRType{NamedTuple(name: String,age: Int32)}defgreet_rb"Hello from Ruby. My name is #{self.name.value}"endcrystallize:stringdefgreet_cr"Hello from Crystal, My name is #{self.name.value}"endendperson=Person.new(name: "Bob",age: 30)putsperson.greet_rbperson.name="Alice"putsperson.greet_cr

Calling Ruby from Crystal

You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with expose_to_crystal so that crystalruby can perform the appropriate type conversions.

require'crystalruby'moduleAdderexpose_to_crystal:int32defadd_rb(a: Int32,b: Int32)a + bendcrystallize:int32defadd_crystal(a: Int32,b: Int32)returnadd_rb(a,b)endendputsAdder.add_crystal(1,2)

Kemal

Here's a more realistic example of where you could call Ruby from Crystal. We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing us to combine the raw speed of Kemal, with the flexibility of Ruby.

require'crystalruby'shard:kemal,github: 'kemalcr/kemal'crystallizeasync: truedefstart_serverKemal.run(3000,[""])endexpose_to_crystaldefreturn_ruby_response(returns: String)"Hello World! #{Random.rand(0..100)}"endcrystaldoget"/kemal_rb"doreturn_ruby_responseendget"/kemal_cr"do"Hello World! #{Random.rand(0..100)}"endendstart_server

We could compare the above to an equivalent pure Ruby implementation using Sinatra.

require'sinatra'get'/sinatra_rb'do'Hello world!'end

and benchmark the two.

$ wrk -d 2 http://localhost:4567/kemal_rb
... Requests/sec: 23352.00
$ wrk -d 2 http://localhost:4567/kemal_cr
... Requests/sec: 35730.03
$ wrk -d 2 http://localhost:4567/sinatra_rb
... Requests/sec: 5300.67

Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation and almost 66% as fast as the pure Crystal implementation.

Yielding

crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal. To support this, you must add a block argument to your method signature, and use the yield keyword to call the block.

See notes on how to define a Proc type in Crystal here

require'crystalruby'crystallizedefyielder_cr(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendexpose_to_crystaldefyielder_rb(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendcrystallizedefinvoke_yielder_rb(a: Int32,b: Int32)yielder_rb(a,b)do |sum|
putssumendendyielder_cr(10,20){|sum| putssum}#=> 30invoke_yielder_rb(50,50)#=> 100

Exceptions

Exceptions thrown in Crystal code can be caught in Ruby.

Using shards

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

  • run shards install for you
  • require the specified shard upon compilation.

If your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.

Wrapping Crystal code in Ruby

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:int32do |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 + 1enddefconvert_to_i_and_add_and_succ(a: :int32,b: :int32)a + bendputsconvert_to_i_and_add_and_succ("1","2")

Inline Chunks

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.

  1. Start our toy project
mkdir crystalredis
cd crystalredis
bundle init
  1. Add dependencies to our Gemfile and run bundle install
# frozen_string_literal: truesource"https://rubygems.org"gem'crystalruby'# Let's see if performance is comparable to that of the redis gem.gem'benchmark-ips'gem'redis'
  1. Write our Redis client
# Filename: crystalredis.rbrequire'crystalruby'moduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendend
  1. Compile and benchmark our new module in Ruby
# Filename: benchmark.rb# Let's compare the performance of our CrystalRedis module to the Ruby Redis gemrequire'crystalruby'require'redis'require'benchmark/ips'require'debug'# For a high IPS single-threaded program, we can set the single_thread_mode to true for faster# FFI interopCrystalRuby.configuredo |config|
config.single_thread_mode=trueendmoduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendendBenchmark.ipsdo |x|
rbredis=Redis.newx.report(:crredis)doCrystalRedis.set("hello","world")CrystalRedis.get("hello")endx.report(:rbredis)dorbredis.set("hello","world")rbredis.get("hello")endend
  1. Run the benchmark
$ bundle exec ruby benchmark.rb

Release Builds

You can control whether crystalruby builds in debug or release mode by setting following config option

CrystalRuby.configuredo |config|
config.debug=falseend

By 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

  • Preloads all files Ruby code with embedded crystal
  • Forces compilation.

E.g.

# E.g. crystalruby_build.rbrequire"crystalruby"CrystalRuby.configuredo |config|
config.debug=falseendrequire_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.

Concurrency

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.

moduleSleepercrystallizedefsleep_syncsleep2.secondsendcrystallizeasync: truedefsleep_asyncsleep2.secondsendend
5.times.map{Thread.new{Sleeper.sleep_sync}}.each(&:join)# Will take 10 seconds5.times.map{Thread.new{Sleeper.sleep_async}}.each(&:join)# Will take 2 seconds (the sleeps are processed concurrently)

Reactor performance

There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 microseconds per call. For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2) then the overhead of the reactor can become significant.

In this case you can use the crystalruby in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that all calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.

CrystalRuby.configuredo |config|
config.single_thread_mode=trueend

Live Reloading

crystalruby supports live reloading of Crystal code. It will intelligently recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.

Multi-library support

Large Crystal projects are known to have long compile times. To mitigate this, crystalruby supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystal code within the project. To indicate which library a piece of embedded Crystal code belongs to, you can use the lib option in the crystallize and crystal methods. If the lib option is not provided, the code will be compiled into the default library (simply named crystalruby).

moduleFoocrystallizelib: "foo"defbarputs"Hello from Foo"endcrystallib: "foo"doREDIS=Redis.newendend

Naturally, Crystal methods must reside in the same library to natively interact. Cross library interaction can be facilitated via Ruby code.

Troubleshooting

In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.

To do this execute:

bundle exec crystalruby clean

Design Goals

crystalruby's primary purpose is to provide ergonomic access to Crystal from Ruby, over FFI. For simple usage, advanced knowledge of Crystal should not be required.

However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.

It should support escape hatches to allow it to coexist with code that performs a more direct FFI integration to implement advanced functionality not supported by crystalruby.

The library is currently in its infancy.

Installation

To get started, add this line to your application's Gemfile:

gem'crystalruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crystalruby

crystalruby supports some basic configuration options, which can be specified inside a crystalruby.yaml file in the root of your project. You can run crystalruby init to generate a configuration file with sane defaults.

$ crystalruby init
crystal_src_dir: "./crystalruby"crystal_codegen_dir: "generated"crystal_main_file: "main.cr"crystal_lib_name: "crlib"crystal_codegen_dir: "generated"debug: true

Alternatively, these can be set programmatically, e.g:

CrystalRuby.configuredo |config|
config.crystal_src_dir="./crystalruby"config.crystal_codegen_dir="generated"config.crystal_missing_ignore=falseconfig.debug=trueconfig.verbose=falseconfig.colorize_log_output=falseconfig.log_level=:infoend

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Embed Crystal code directly in Ruby

Resources

Code of conduct

Stars

653 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

logo

crystalruby

GEM Version
GEM Downloads

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.crystallizedefadd(a: Int32,b: Int32,returns: Int32)a + bend# This method is run in Crystal, not Ruby!putsadd(1,2)# => 3

With 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.

require'crystalruby'require'benchmark'crystallize:int32defcount_primes_upto_cr(n: Int32)(2..n).each.countdo |i|
is_prime=true(2..Math.isqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendenddefcount_primes_upto_rb(n)(2..n).each.countdo |i|
is_prime=true(2..Integer.sqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendendputsBenchmark.realtime{count_primes_upto_rb(1_000_000)}putsBenchmark.realtime{count_primes_upto_cr(1_000_000)}
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'moduleCacheshard:redis,github: 'jgaskins/redis'crystallize:stringdefredis_get(key: String)rds=Redis::Client.newvalue=rds.get(key).to_sendcrystallize:stringdefredis_set_and_return(key: String,value: String)redis=Redis::Client.newredis.set(key,value)Cache.redis_get(key)endendCache.redis_set_and_return('test','abc')putsCache.redis_get('test')
$ abc

Syntax

To 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.

Method Signatures

Parameter types are defined using kwarg syntax, with the type as the value. E.g.

deffoo(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 Int32crystallize->{Int32}defreturns_int323end# You can use the symbol shortcode for primitive typescrystallize:int32defreturns_int323end# Define the return type directly using the `returns` kwargcrystallizedefreturns_int32(returns: Int32)3end

Ruby Compatible Method Bodies

Where 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:intdefadd(a: :int,b: :int)puts"Adding #{a} and #{b}"a + bend

Crystal-only Syntax

Some Crystal syntax is not valid Ruby, for methods of this form, we need to define our functions using the raw: true option

crystallizeraw: truedefadd(a: :int,b: :int)<<~CRYSTAL c = 0_u64 a + b + c CRYSTALend

Upgrading from version 0.2.x

Change in type signatures

In version 0.2.x, argument and return types were passed to the crystallize method using a different syntax:

# V <= 0.2.xcrystallize[arg1: :arg1_type,arg2: :arg2_type]=>:return_typedeffoo(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.xcrystallize:return_typedeffoo(arg1: :arg1_type,arg2: :arg2_type)# OR use the `returns` kwargcrystallizedeffoo(arg1: :arg1_type,arg2: :arg2_type,returns: :return_type)

Getting Started

The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.

# crystalrubytest.rbrequire'bundler/inline'gemfiledosource'https://rubygems.org'gem'crystalruby'endrequire'crystalruby'crystallize:intdefadd(a: :int,b: :int)a + bendputsadd(1,2)

Types

Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.

Supported Types

  • UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
  • Time
  • Symbol
  • Nil
  • Bool
  • Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
  • Proc

Primitive types short-hands

  • :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
  • :int :uint :int32 :uint32 :long :ulong :int64 :uint64
  • :long_long :ulong_long :float :double :bool
  • :void :pointer :string

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'crystallizedefcomplex_argument_types(a: Int64 | Float64 | Nil,b: String | Array(Bool))puts"Got #{a} and #{b}"endcrystallizedefcomplex_return_type(returns: Int32 | String | Hash(String,Array(NamedTuple(hello: Int32)) | Time))return{"hello"=>[{hello: 1,},],"world"=>Time.utc}endcomplex_argument_types(10,"Hello")putscomplex_return_type()

Type signatures validations are applied to both arguments and return types.

[1]pry(main)> complex_argument_types(nil,"test")Gotandtest=>nil[2]pry(main)> complex_argument_types(88,[true,false,true])Got88and[true,false,true]=>nil[3]pry(main)> complex_argument_types(88,[true,false,88])ArgumentError: ExpectedBoolbutwasIntatline1,column15fromcrystalruby.rb:303:in `blockin compile!'

Reference Types

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)}crystallizedefmethod_with_named_types(a: IntArrOrBoolArr,returns: IntArrOrBoolArr)returnaend# 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 referenceint_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)}crystallizedefarray_doubler(a: IntArray)a.map!{ |x| x * 2}enddefarray_doubler_rb(a)a.map!{ |x| x * 2}endbig_array=Array.new(1_000_000){rand(100)}big_int_array=IntArray.new(big_array)Benchmark.bmdo |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)}end

Shared Instances

You 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'classPerson < CRType{NamedTuple(name: String,age: Int32)}defgreet_rb"Hello from Ruby. My name is #{self.name.value}"endcrystallize:stringdefgreet_cr"Hello from Crystal, My name is #{self.name.value}"endendperson=Person.new(name: "Bob",age: 30)putsperson.greet_rbperson.name="Alice"putsperson.greet_cr

Calling Ruby from Crystal

You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with expose_to_crystal so that crystalruby can perform the appropriate type conversions.

require'crystalruby'moduleAdderexpose_to_crystal:int32defadd_rb(a: Int32,b: Int32)a + bendcrystallize:int32defadd_crystal(a: Int32,b: Int32)returnadd_rb(a,b)endendputsAdder.add_crystal(1,2)

Kemal

Here's a more realistic example of where you could call Ruby from Crystal. We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing us to combine the raw speed of Kemal, with the flexibility of Ruby.

require'crystalruby'shard:kemal,github: 'kemalcr/kemal'crystallizeasync: truedefstart_serverKemal.run(3000,[""])endexpose_to_crystaldefreturn_ruby_response(returns: String)"Hello World! #{Random.rand(0..100)}"endcrystaldoget"/kemal_rb"doreturn_ruby_responseendget"/kemal_cr"do"Hello World! #{Random.rand(0..100)}"endendstart_server

We could compare the above to an equivalent pure Ruby implementation using Sinatra.

require'sinatra'get'/sinatra_rb'do'Hello world!'end

and benchmark the two.

$ wrk -d 2 http://localhost:4567/kemal_rb
... Requests/sec: 23352.00
$ wrk -d 2 http://localhost:4567/kemal_cr
... Requests/sec: 35730.03
$ wrk -d 2 http://localhost:4567/sinatra_rb
... Requests/sec: 5300.67

Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation and almost 66% as fast as the pure Crystal implementation.

Yielding

crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal. To support this, you must add a block argument to your method signature, and use the yield keyword to call the block.

See notes on how to define a Proc type in Crystal here

require'crystalruby'crystallizedefyielder_cr(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendexpose_to_crystaldefyielder_rb(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendcrystallizedefinvoke_yielder_rb(a: Int32,b: Int32)yielder_rb(a,b)do |sum|
putssumendendyielder_cr(10,20){|sum| putssum}#=> 30invoke_yielder_rb(50,50)#=> 100

Exceptions

Exceptions thrown in Crystal code can be caught in Ruby.

Using shards

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

  • run shards install for you
  • require the specified shard upon compilation.

If your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.

Wrapping Crystal code in Ruby

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:int32do |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 + 1enddefconvert_to_i_and_add_and_succ(a: :int32,b: :int32)a + bendputsconvert_to_i_and_add_and_succ("1","2")

Inline Chunks

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.

  1. Start our toy project
mkdir crystalredis
cd crystalredis
bundle init
  1. Add dependencies to our Gemfile and run bundle install
# frozen_string_literal: truesource"https://rubygems.org"gem'crystalruby'# Let's see if performance is comparable to that of the redis gem.gem'benchmark-ips'gem'redis'
  1. Write our Redis client
# Filename: crystalredis.rbrequire'crystalruby'moduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendend
  1. Compile and benchmark our new module in Ruby
# Filename: benchmark.rb# Let's compare the performance of our CrystalRedis module to the Ruby Redis gemrequire'crystalruby'require'redis'require'benchmark/ips'require'debug'# For a high IPS single-threaded program, we can set the single_thread_mode to true for faster# FFI interopCrystalRuby.configuredo |config|
config.single_thread_mode=trueendmoduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendendBenchmark.ipsdo |x|
rbredis=Redis.newx.report(:crredis)doCrystalRedis.set("hello","world")CrystalRedis.get("hello")endx.report(:rbredis)dorbredis.set("hello","world")rbredis.get("hello")endend
  1. Run the benchmark
$ bundle exec ruby benchmark.rb

Release Builds

You can control whether crystalruby builds in debug or release mode by setting following config option

CrystalRuby.configuredo |config|
config.debug=falseend

By 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

  • Preloads all files Ruby code with embedded crystal
  • Forces compilation.

E.g.

# E.g. crystalruby_build.rbrequire"crystalruby"CrystalRuby.configuredo |config|
config.debug=falseendrequire_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.

Concurrency

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.

moduleSleepercrystallizedefsleep_syncsleep2.secondsendcrystallizeasync: truedefsleep_asyncsleep2.secondsendend
5.times.map{Thread.new{Sleeper.sleep_sync}}.each(&:join)# Will take 10 seconds5.times.map{Thread.new{Sleeper.sleep_async}}.each(&:join)# Will take 2 seconds (the sleeps are processed concurrently)

Reactor performance

There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 microseconds per call. For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2) then the overhead of the reactor can become significant.

In this case you can use the crystalruby in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that all calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.

CrystalRuby.configuredo |config|
config.single_thread_mode=trueend

Live Reloading

crystalruby supports live reloading of Crystal code. It will intelligently recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.

Multi-library support

Large Crystal projects are known to have long compile times. To mitigate this, crystalruby supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystal code within the project. To indicate which library a piece of embedded Crystal code belongs to, you can use the lib option in the crystallize and crystal methods. If the lib option is not provided, the code will be compiled into the default library (simply named crystalruby).

moduleFoocrystallizelib: "foo"defbarputs"Hello from Foo"endcrystallib: "foo"doREDIS=Redis.newendend

Naturally, Crystal methods must reside in the same library to natively interact. Cross library interaction can be facilitated via Ruby code.

Troubleshooting

In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.

To do this execute:

bundle exec crystalruby clean

Design Goals

crystalruby's primary purpose is to provide ergonomic access to Crystal from Ruby, over FFI. For simple usage, advanced knowledge of Crystal should not be required.

However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.

It should support escape hatches to allow it to coexist with code that performs a more direct FFI integration to implement advanced functionality not supported by crystalruby.

The library is currently in its infancy.

Installation

To get started, add this line to your application's Gemfile:

gem'crystalruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crystalruby

crystalruby supports some basic configuration options, which can be specified inside a crystalruby.yaml file in the root of your project. You can run crystalruby init to generate a configuration file with sane defaults.

$ crystalruby init
crystal_src_dir: "./crystalruby"crystal_codegen_dir: "generated"crystal_main_file: "main.cr"crystal_lib_name: "crlib"crystal_codegen_dir: "generated"debug: true

Alternatively, these can be set programmatically, e.g:

CrystalRuby.configuredo |config|
config.crystal_src_dir="./crystalruby"config.crystal_codegen_dir="generated"config.crystal_missing_ignore=falseconfig.debug=trueconfig.verbose=falseconfig.colorize_log_output=falseconfig.log_level=:infoend

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Embed Crystal code directly in Ruby

Resources

Code of conduct

Stars

653 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

logo

crystalruby

GEM Version
GEM Downloads

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.crystallizedefadd(a: Int32,b: Int32,returns: Int32)a + bend# This method is run in Crystal, not Ruby!putsadd(1,2)# => 3

With 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.

require'crystalruby'require'benchmark'crystallize:int32defcount_primes_upto_cr(n: Int32)(2..n).each.countdo |i|
is_prime=true(2..Math.isqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendenddefcount_primes_upto_rb(n)(2..n).each.countdo |i|
is_prime=true(2..Integer.sqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendendputsBenchmark.realtime{count_primes_upto_rb(1_000_000)}putsBenchmark.realtime{count_primes_upto_cr(1_000_000)}
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'moduleCacheshard:redis,github: 'jgaskins/redis'crystallize:stringdefredis_get(key: String)rds=Redis::Client.newvalue=rds.get(key).to_sendcrystallize:stringdefredis_set_and_return(key: String,value: String)redis=Redis::Client.newredis.set(key,value)Cache.redis_get(key)endendCache.redis_set_and_return('test','abc')putsCache.redis_get('test')
$ abc

Syntax

To 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.

Method Signatures

Parameter types are defined using kwarg syntax, with the type as the value. E.g.

deffoo(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 Int32crystallize->{Int32}defreturns_int323end# You can use the symbol shortcode for primitive typescrystallize:int32defreturns_int323end# Define the return type directly using the `returns` kwargcrystallizedefreturns_int32(returns: Int32)3end

Ruby Compatible Method Bodies

Where 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:intdefadd(a: :int,b: :int)puts"Adding #{a} and #{b}"a + bend

Crystal-only Syntax

Some Crystal syntax is not valid Ruby, for methods of this form, we need to define our functions using the raw: true option

crystallizeraw: truedefadd(a: :int,b: :int)<<~CRYSTAL c = 0_u64 a + b + c CRYSTALend

Upgrading from version 0.2.x

Change in type signatures

In version 0.2.x, argument and return types were passed to the crystallize method using a different syntax:

# V <= 0.2.xcrystallize[arg1: :arg1_type,arg2: :arg2_type]=>:return_typedeffoo(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.xcrystallize:return_typedeffoo(arg1: :arg1_type,arg2: :arg2_type)# OR use the `returns` kwargcrystallizedeffoo(arg1: :arg1_type,arg2: :arg2_type,returns: :return_type)

Getting Started

The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.

# crystalrubytest.rbrequire'bundler/inline'gemfiledosource'https://rubygems.org'gem'crystalruby'endrequire'crystalruby'crystallize:intdefadd(a: :int,b: :int)a + bendputsadd(1,2)

Types

Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.

Supported Types

  • UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
  • Time
  • Symbol
  • Nil
  • Bool
  • Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
  • Proc

Primitive types short-hands

  • :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
  • :int :uint :int32 :uint32 :long :ulong :int64 :uint64
  • :long_long :ulong_long :float :double :bool
  • :void :pointer :string

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'crystallizedefcomplex_argument_types(a: Int64 | Float64 | Nil,b: String | Array(Bool))puts"Got #{a} and #{b}"endcrystallizedefcomplex_return_type(returns: Int32 | String | Hash(String,Array(NamedTuple(hello: Int32)) | Time))return{"hello"=>[{hello: 1,},],"world"=>Time.utc}endcomplex_argument_types(10,"Hello")putscomplex_return_type()

Type signatures validations are applied to both arguments and return types.

[1]pry(main)> complex_argument_types(nil,"test")Gotandtest=>nil[2]pry(main)> complex_argument_types(88,[true,false,true])Got88and[true,false,true]=>nil[3]pry(main)> complex_argument_types(88,[true,false,88])ArgumentError: ExpectedBoolbutwasIntatline1,column15fromcrystalruby.rb:303:in `blockin compile!'

Reference Types

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)}crystallizedefmethod_with_named_types(a: IntArrOrBoolArr,returns: IntArrOrBoolArr)returnaend# 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 referenceint_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)}crystallizedefarray_doubler(a: IntArray)a.map!{ |x| x * 2}enddefarray_doubler_rb(a)a.map!{ |x| x * 2}endbig_array=Array.new(1_000_000){rand(100)}big_int_array=IntArray.new(big_array)Benchmark.bmdo |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)}end

Shared Instances

You 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'classPerson < CRType{NamedTuple(name: String,age: Int32)}defgreet_rb"Hello from Ruby. My name is #{self.name.value}"endcrystallize:stringdefgreet_cr"Hello from Crystal, My name is #{self.name.value}"endendperson=Person.new(name: "Bob",age: 30)putsperson.greet_rbperson.name="Alice"putsperson.greet_cr

Calling Ruby from Crystal

You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with expose_to_crystal so that crystalruby can perform the appropriate type conversions.

require'crystalruby'moduleAdderexpose_to_crystal:int32defadd_rb(a: Int32,b: Int32)a + bendcrystallize:int32defadd_crystal(a: Int32,b: Int32)returnadd_rb(a,b)endendputsAdder.add_crystal(1,2)

Kemal

Here's a more realistic example of where you could call Ruby from Crystal. We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing us to combine the raw speed of Kemal, with the flexibility of Ruby.

require'crystalruby'shard:kemal,github: 'kemalcr/kemal'crystallizeasync: truedefstart_serverKemal.run(3000,[""])endexpose_to_crystaldefreturn_ruby_response(returns: String)"Hello World! #{Random.rand(0..100)}"endcrystaldoget"/kemal_rb"doreturn_ruby_responseendget"/kemal_cr"do"Hello World! #{Random.rand(0..100)}"endendstart_server

We could compare the above to an equivalent pure Ruby implementation using Sinatra.

require'sinatra'get'/sinatra_rb'do'Hello world!'end

and benchmark the two.

$ wrk -d 2 http://localhost:4567/kemal_rb
... Requests/sec: 23352.00
$ wrk -d 2 http://localhost:4567/kemal_cr
... Requests/sec: 35730.03
$ wrk -d 2 http://localhost:4567/sinatra_rb
... Requests/sec: 5300.67

Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation and almost 66% as fast as the pure Crystal implementation.

Yielding

crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal. To support this, you must add a block argument to your method signature, and use the yield keyword to call the block.

See notes on how to define a Proc type in Crystal here

require'crystalruby'crystallizedefyielder_cr(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendexpose_to_crystaldefyielder_rb(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendcrystallizedefinvoke_yielder_rb(a: Int32,b: Int32)yielder_rb(a,b)do |sum|
putssumendendyielder_cr(10,20){|sum| putssum}#=> 30invoke_yielder_rb(50,50)#=> 100

Exceptions

Exceptions thrown in Crystal code can be caught in Ruby.

Using shards

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

  • run shards install for you
  • require the specified shard upon compilation.

If your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.

Wrapping Crystal code in Ruby

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:int32do |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 + 1enddefconvert_to_i_and_add_and_succ(a: :int32,b: :int32)a + bendputsconvert_to_i_and_add_and_succ("1","2")

Inline Chunks

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.

  1. Start our toy project
mkdir crystalredis
cd crystalredis
bundle init
  1. Add dependencies to our Gemfile and run bundle install
# frozen_string_literal: truesource"https://rubygems.org"gem'crystalruby'# Let's see if performance is comparable to that of the redis gem.gem'benchmark-ips'gem'redis'
  1. Write our Redis client
# Filename: crystalredis.rbrequire'crystalruby'moduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendend
  1. Compile and benchmark our new module in Ruby
# Filename: benchmark.rb# Let's compare the performance of our CrystalRedis module to the Ruby Redis gemrequire'crystalruby'require'redis'require'benchmark/ips'require'debug'# For a high IPS single-threaded program, we can set the single_thread_mode to true for faster# FFI interopCrystalRuby.configuredo |config|
config.single_thread_mode=trueendmoduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendendBenchmark.ipsdo |x|
rbredis=Redis.newx.report(:crredis)doCrystalRedis.set("hello","world")CrystalRedis.get("hello")endx.report(:rbredis)dorbredis.set("hello","world")rbredis.get("hello")endend
  1. Run the benchmark
$ bundle exec ruby benchmark.rb

Release Builds

You can control whether crystalruby builds in debug or release mode by setting following config option

CrystalRuby.configuredo |config|
config.debug=falseend

By 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

  • Preloads all files Ruby code with embedded crystal
  • Forces compilation.

E.g.

# E.g. crystalruby_build.rbrequire"crystalruby"CrystalRuby.configuredo |config|
config.debug=falseendrequire_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.

Concurrency

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.

moduleSleepercrystallizedefsleep_syncsleep2.secondsendcrystallizeasync: truedefsleep_asyncsleep2.secondsendend
5.times.map{Thread.new{Sleeper.sleep_sync}}.each(&:join)# Will take 10 seconds5.times.map{Thread.new{Sleeper.sleep_async}}.each(&:join)# Will take 2 seconds (the sleeps are processed concurrently)

Reactor performance

There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 microseconds per call. For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2) then the overhead of the reactor can become significant.

In this case you can use the crystalruby in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that all calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.

CrystalRuby.configuredo |config|
config.single_thread_mode=trueend

Live Reloading

crystalruby supports live reloading of Crystal code. It will intelligently recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.

Multi-library support

Large Crystal projects are known to have long compile times. To mitigate this, crystalruby supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystal code within the project. To indicate which library a piece of embedded Crystal code belongs to, you can use the lib option in the crystallize and crystal methods. If the lib option is not provided, the code will be compiled into the default library (simply named crystalruby).

moduleFoocrystallizelib: "foo"defbarputs"Hello from Foo"endcrystallib: "foo"doREDIS=Redis.newendend

Naturally, Crystal methods must reside in the same library to natively interact. Cross library interaction can be facilitated via Ruby code.

Troubleshooting

In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.

To do this execute:

bundle exec crystalruby clean

Design Goals

crystalruby's primary purpose is to provide ergonomic access to Crystal from Ruby, over FFI. For simple usage, advanced knowledge of Crystal should not be required.

However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.

It should support escape hatches to allow it to coexist with code that performs a more direct FFI integration to implement advanced functionality not supported by crystalruby.

The library is currently in its infancy.

Installation

To get started, add this line to your application's Gemfile:

gem'crystalruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crystalruby

crystalruby supports some basic configuration options, which can be specified inside a crystalruby.yaml file in the root of your project. You can run crystalruby init to generate a configuration file with sane defaults.

$ crystalruby init
crystal_src_dir: "./crystalruby"crystal_codegen_dir: "generated"crystal_main_file: "main.cr"crystal_lib_name: "crlib"crystal_codegen_dir: "generated"debug: true

Alternatively, these can be set programmatically, e.g:

CrystalRuby.configuredo |config|
config.crystal_src_dir="./crystalruby"config.crystal_codegen_dir="generated"config.crystal_missing_ignore=falseconfig.debug=trueconfig.verbose=falseconfig.colorize_log_output=falseconfig.log_level=:infoend

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Embed Crystal code directly in Ruby

Resources

Code of conduct

Stars

653 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

logo

crystalruby

GEM Version
GEM Downloads

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.crystallizedefadd(a: Int32,b: Int32,returns: Int32)a + bend# This method is run in Crystal, not Ruby!putsadd(1,2)# => 3

With 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.

require'crystalruby'require'benchmark'crystallize:int32defcount_primes_upto_cr(n: Int32)(2..n).each.countdo |i|
is_prime=true(2..Math.isqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendenddefcount_primes_upto_rb(n)(2..n).each.countdo |i|
is_prime=true(2..Integer.sqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendendputsBenchmark.realtime{count_primes_upto_rb(1_000_000)}putsBenchmark.realtime{count_primes_upto_cr(1_000_000)}
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'moduleCacheshard:redis,github: 'jgaskins/redis'crystallize:stringdefredis_get(key: String)rds=Redis::Client.newvalue=rds.get(key).to_sendcrystallize:stringdefredis_set_and_return(key: String,value: String)redis=Redis::Client.newredis.set(key,value)Cache.redis_get(key)endendCache.redis_set_and_return('test','abc')putsCache.redis_get('test')
$ abc

Syntax

To 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.

Method Signatures

Parameter types are defined using kwarg syntax, with the type as the value. E.g.

deffoo(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 Int32crystallize->{Int32}defreturns_int323end# You can use the symbol shortcode for primitive typescrystallize:int32defreturns_int323end# Define the return type directly using the `returns` kwargcrystallizedefreturns_int32(returns: Int32)3end

Ruby Compatible Method Bodies

Where 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:intdefadd(a: :int,b: :int)puts"Adding #{a} and #{b}"a + bend

Crystal-only Syntax

Some Crystal syntax is not valid Ruby, for methods of this form, we need to define our functions using the raw: true option

crystallizeraw: truedefadd(a: :int,b: :int)<<~CRYSTAL c = 0_u64 a + b + c CRYSTALend

Upgrading from version 0.2.x

Change in type signatures

In version 0.2.x, argument and return types were passed to the crystallize method using a different syntax:

# V <= 0.2.xcrystallize[arg1: :arg1_type,arg2: :arg2_type]=>:return_typedeffoo(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.xcrystallize:return_typedeffoo(arg1: :arg1_type,arg2: :arg2_type)# OR use the `returns` kwargcrystallizedeffoo(arg1: :arg1_type,arg2: :arg2_type,returns: :return_type)

Getting Started

The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.

# crystalrubytest.rbrequire'bundler/inline'gemfiledosource'https://rubygems.org'gem'crystalruby'endrequire'crystalruby'crystallize:intdefadd(a: :int,b: :int)a + bendputsadd(1,2)

Types

Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.

Supported Types

  • UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
  • Time
  • Symbol
  • Nil
  • Bool
  • Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
  • Proc

Primitive types short-hands

  • :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
  • :int :uint :int32 :uint32 :long :ulong :int64 :uint64
  • :long_long :ulong_long :float :double :bool
  • :void :pointer :string

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'crystallizedefcomplex_argument_types(a: Int64 | Float64 | Nil,b: String | Array(Bool))puts"Got #{a} and #{b}"endcrystallizedefcomplex_return_type(returns: Int32 | String | Hash(String,Array(NamedTuple(hello: Int32)) | Time))return{"hello"=>[{hello: 1,},],"world"=>Time.utc}endcomplex_argument_types(10,"Hello")putscomplex_return_type()

Type signatures validations are applied to both arguments and return types.

[1]pry(main)> complex_argument_types(nil,"test")Gotandtest=>nil[2]pry(main)> complex_argument_types(88,[true,false,true])Got88and[true,false,true]=>nil[3]pry(main)> complex_argument_types(88,[true,false,88])ArgumentError: ExpectedBoolbutwasIntatline1,column15fromcrystalruby.rb:303:in `blockin compile!'

Reference Types

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)}crystallizedefmethod_with_named_types(a: IntArrOrBoolArr,returns: IntArrOrBoolArr)returnaend# 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 referenceint_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)}crystallizedefarray_doubler(a: IntArray)a.map!{ |x| x * 2}enddefarray_doubler_rb(a)a.map!{ |x| x * 2}endbig_array=Array.new(1_000_000){rand(100)}big_int_array=IntArray.new(big_array)Benchmark.bmdo |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)}end

Shared Instances

You 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'classPerson < CRType{NamedTuple(name: String,age: Int32)}defgreet_rb"Hello from Ruby. My name is #{self.name.value}"endcrystallize:stringdefgreet_cr"Hello from Crystal, My name is #{self.name.value}"endendperson=Person.new(name: "Bob",age: 30)putsperson.greet_rbperson.name="Alice"putsperson.greet_cr

Calling Ruby from Crystal

You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with expose_to_crystal so that crystalruby can perform the appropriate type conversions.

require'crystalruby'moduleAdderexpose_to_crystal:int32defadd_rb(a: Int32,b: Int32)a + bendcrystallize:int32defadd_crystal(a: Int32,b: Int32)returnadd_rb(a,b)endendputsAdder.add_crystal(1,2)

Kemal

Here's a more realistic example of where you could call Ruby from Crystal. We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing us to combine the raw speed of Kemal, with the flexibility of Ruby.

require'crystalruby'shard:kemal,github: 'kemalcr/kemal'crystallizeasync: truedefstart_serverKemal.run(3000,[""])endexpose_to_crystaldefreturn_ruby_response(returns: String)"Hello World! #{Random.rand(0..100)}"endcrystaldoget"/kemal_rb"doreturn_ruby_responseendget"/kemal_cr"do"Hello World! #{Random.rand(0..100)}"endendstart_server

We could compare the above to an equivalent pure Ruby implementation using Sinatra.

require'sinatra'get'/sinatra_rb'do'Hello world!'end

and benchmark the two.

$ wrk -d 2 http://localhost:4567/kemal_rb
... Requests/sec: 23352.00
$ wrk -d 2 http://localhost:4567/kemal_cr
... Requests/sec: 35730.03
$ wrk -d 2 http://localhost:4567/sinatra_rb
... Requests/sec: 5300.67

Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation and almost 66% as fast as the pure Crystal implementation.

Yielding

crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal. To support this, you must add a block argument to your method signature, and use the yield keyword to call the block.

See notes on how to define a Proc type in Crystal here

require'crystalruby'crystallizedefyielder_cr(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendexpose_to_crystaldefyielder_rb(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendcrystallizedefinvoke_yielder_rb(a: Int32,b: Int32)yielder_rb(a,b)do |sum|
putssumendendyielder_cr(10,20){|sum| putssum}#=> 30invoke_yielder_rb(50,50)#=> 100

Exceptions

Exceptions thrown in Crystal code can be caught in Ruby.

Using shards

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

  • run shards install for you
  • require the specified shard upon compilation.

If your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.

Wrapping Crystal code in Ruby

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:int32do |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 + 1enddefconvert_to_i_and_add_and_succ(a: :int32,b: :int32)a + bendputsconvert_to_i_and_add_and_succ("1","2")

Inline Chunks

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.

  1. Start our toy project
mkdir crystalredis
cd crystalredis
bundle init
  1. Add dependencies to our Gemfile and run bundle install
# frozen_string_literal: truesource"https://rubygems.org"gem'crystalruby'# Let's see if performance is comparable to that of the redis gem.gem'benchmark-ips'gem'redis'
  1. Write our Redis client
# Filename: crystalredis.rbrequire'crystalruby'moduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendend
  1. Compile and benchmark our new module in Ruby
# Filename: benchmark.rb# Let's compare the performance of our CrystalRedis module to the Ruby Redis gemrequire'crystalruby'require'redis'require'benchmark/ips'require'debug'# For a high IPS single-threaded program, we can set the single_thread_mode to true for faster# FFI interopCrystalRuby.configuredo |config|
config.single_thread_mode=trueendmoduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendendBenchmark.ipsdo |x|
rbredis=Redis.newx.report(:crredis)doCrystalRedis.set("hello","world")CrystalRedis.get("hello")endx.report(:rbredis)dorbredis.set("hello","world")rbredis.get("hello")endend
  1. Run the benchmark
$ bundle exec ruby benchmark.rb

Release Builds

You can control whether crystalruby builds in debug or release mode by setting following config option

CrystalRuby.configuredo |config|
config.debug=falseend

By 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

  • Preloads all files Ruby code with embedded crystal
  • Forces compilation.

E.g.

# E.g. crystalruby_build.rbrequire"crystalruby"CrystalRuby.configuredo |config|
config.debug=falseendrequire_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.

Concurrency

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.

moduleSleepercrystallizedefsleep_syncsleep2.secondsendcrystallizeasync: truedefsleep_asyncsleep2.secondsendend
5.times.map{Thread.new{Sleeper.sleep_sync}}.each(&:join)# Will take 10 seconds5.times.map{Thread.new{Sleeper.sleep_async}}.each(&:join)# Will take 2 seconds (the sleeps are processed concurrently)

Reactor performance

There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 microseconds per call. For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2) then the overhead of the reactor can become significant.

In this case you can use the crystalruby in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that all calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.

CrystalRuby.configuredo |config|
config.single_thread_mode=trueend

Live Reloading

crystalruby supports live reloading of Crystal code. It will intelligently recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.

Multi-library support

Large Crystal projects are known to have long compile times. To mitigate this, crystalruby supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystal code within the project. To indicate which library a piece of embedded Crystal code belongs to, you can use the lib option in the crystallize and crystal methods. If the lib option is not provided, the code will be compiled into the default library (simply named crystalruby).

moduleFoocrystallizelib: "foo"defbarputs"Hello from Foo"endcrystallib: "foo"doREDIS=Redis.newendend

Naturally, Crystal methods must reside in the same library to natively interact. Cross library interaction can be facilitated via Ruby code.

Troubleshooting

In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.

To do this execute:

bundle exec crystalruby clean

Design Goals

crystalruby's primary purpose is to provide ergonomic access to Crystal from Ruby, over FFI. For simple usage, advanced knowledge of Crystal should not be required.

However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.

It should support escape hatches to allow it to coexist with code that performs a more direct FFI integration to implement advanced functionality not supported by crystalruby.

The library is currently in its infancy.

Installation

To get started, add this line to your application's Gemfile:

gem'crystalruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crystalruby

crystalruby supports some basic configuration options, which can be specified inside a crystalruby.yaml file in the root of your project. You can run crystalruby init to generate a configuration file with sane defaults.

$ crystalruby init
crystal_src_dir: "./crystalruby"crystal_codegen_dir: "generated"crystal_main_file: "main.cr"crystal_lib_name: "crlib"crystal_codegen_dir: "generated"debug: true

Alternatively, these can be set programmatically, e.g:

CrystalRuby.configuredo |config|
config.crystal_src_dir="./crystalruby"config.crystal_codegen_dir="generated"config.crystal_missing_ignore=falseconfig.debug=trueconfig.verbose=falseconfig.colorize_log_output=falseconfig.log_level=:infoend

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Embed Crystal code directly in Ruby

Resources

Code of conduct

Stars

653 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

logo

crystalruby

GEM Version
GEM Downloads

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.crystallizedefadd(a: Int32,b: Int32,returns: Int32)a + bend# This method is run in Crystal, not Ruby!putsadd(1,2)# => 3

With 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.

require'crystalruby'require'benchmark'crystallize:int32defcount_primes_upto_cr(n: Int32)(2..n).each.countdo |i|
is_prime=true(2..Math.isqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendenddefcount_primes_upto_rb(n)(2..n).each.countdo |i|
is_prime=true(2..Integer.sqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendendputsBenchmark.realtime{count_primes_upto_rb(1_000_000)}putsBenchmark.realtime{count_primes_upto_cr(1_000_000)}
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'moduleCacheshard:redis,github: 'jgaskins/redis'crystallize:stringdefredis_get(key: String)rds=Redis::Client.newvalue=rds.get(key).to_sendcrystallize:stringdefredis_set_and_return(key: String,value: String)redis=Redis::Client.newredis.set(key,value)Cache.redis_get(key)endendCache.redis_set_and_return('test','abc')putsCache.redis_get('test')
$ abc

Syntax

To 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.

Method Signatures

Parameter types are defined using kwarg syntax, with the type as the value. E.g.

deffoo(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 Int32crystallize->{Int32}defreturns_int323end# You can use the symbol shortcode for primitive typescrystallize:int32defreturns_int323end# Define the return type directly using the `returns` kwargcrystallizedefreturns_int32(returns: Int32)3end

Ruby Compatible Method Bodies

Where 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:intdefadd(a: :int,b: :int)puts"Adding #{a} and #{b}"a + bend

Crystal-only Syntax

Some Crystal syntax is not valid Ruby, for methods of this form, we need to define our functions using the raw: true option

crystallizeraw: truedefadd(a: :int,b: :int)<<~CRYSTAL c = 0_u64 a + b + c CRYSTALend

Upgrading from version 0.2.x

Change in type signatures

In version 0.2.x, argument and return types were passed to the crystallize method using a different syntax:

# V <= 0.2.xcrystallize[arg1: :arg1_type,arg2: :arg2_type]=>:return_typedeffoo(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.xcrystallize:return_typedeffoo(arg1: :arg1_type,arg2: :arg2_type)# OR use the `returns` kwargcrystallizedeffoo(arg1: :arg1_type,arg2: :arg2_type,returns: :return_type)

Getting Started

The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.

# crystalrubytest.rbrequire'bundler/inline'gemfiledosource'https://rubygems.org'gem'crystalruby'endrequire'crystalruby'crystallize:intdefadd(a: :int,b: :int)a + bendputsadd(1,2)

Types

Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.

Supported Types

  • UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
  • Time
  • Symbol
  • Nil
  • Bool
  • Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
  • Proc

Primitive types short-hands

  • :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
  • :int :uint :int32 :uint32 :long :ulong :int64 :uint64
  • :long_long :ulong_long :float :double :bool
  • :void :pointer :string

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'crystallizedefcomplex_argument_types(a: Int64 | Float64 | Nil,b: String | Array(Bool))puts"Got #{a} and #{b}"endcrystallizedefcomplex_return_type(returns: Int32 | String | Hash(String,Array(NamedTuple(hello: Int32)) | Time))return{"hello"=>[{hello: 1,},],"world"=>Time.utc}endcomplex_argument_types(10,"Hello")putscomplex_return_type()

Type signatures validations are applied to both arguments and return types.

[1]pry(main)> complex_argument_types(nil,"test")Gotandtest=>nil[2]pry(main)> complex_argument_types(88,[true,false,true])Got88and[true,false,true]=>nil[3]pry(main)> complex_argument_types(88,[true,false,88])ArgumentError: ExpectedBoolbutwasIntatline1,column15fromcrystalruby.rb:303:in `blockin compile!'

Reference Types

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)}crystallizedefmethod_with_named_types(a: IntArrOrBoolArr,returns: IntArrOrBoolArr)returnaend# 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 referenceint_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)}crystallizedefarray_doubler(a: IntArray)a.map!{ |x| x * 2}enddefarray_doubler_rb(a)a.map!{ |x| x * 2}endbig_array=Array.new(1_000_000){rand(100)}big_int_array=IntArray.new(big_array)Benchmark.bmdo |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)}end

Shared Instances

You 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'classPerson < CRType{NamedTuple(name: String,age: Int32)}defgreet_rb"Hello from Ruby. My name is #{self.name.value}"endcrystallize:stringdefgreet_cr"Hello from Crystal, My name is #{self.name.value}"endendperson=Person.new(name: "Bob",age: 30)putsperson.greet_rbperson.name="Alice"putsperson.greet_cr

Calling Ruby from Crystal

You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with expose_to_crystal so that crystalruby can perform the appropriate type conversions.

require'crystalruby'moduleAdderexpose_to_crystal:int32defadd_rb(a: Int32,b: Int32)a + bendcrystallize:int32defadd_crystal(a: Int32,b: Int32)returnadd_rb(a,b)endendputsAdder.add_crystal(1,2)

Kemal

Here's a more realistic example of where you could call Ruby from Crystal. We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing us to combine the raw speed of Kemal, with the flexibility of Ruby.

require'crystalruby'shard:kemal,github: 'kemalcr/kemal'crystallizeasync: truedefstart_serverKemal.run(3000,[""])endexpose_to_crystaldefreturn_ruby_response(returns: String)"Hello World! #{Random.rand(0..100)}"endcrystaldoget"/kemal_rb"doreturn_ruby_responseendget"/kemal_cr"do"Hello World! #{Random.rand(0..100)}"endendstart_server

We could compare the above to an equivalent pure Ruby implementation using Sinatra.

require'sinatra'get'/sinatra_rb'do'Hello world!'end

and benchmark the two.

$ wrk -d 2 http://localhost:4567/kemal_rb
... Requests/sec: 23352.00
$ wrk -d 2 http://localhost:4567/kemal_cr
... Requests/sec: 35730.03
$ wrk -d 2 http://localhost:4567/sinatra_rb
... Requests/sec: 5300.67

Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation and almost 66% as fast as the pure Crystal implementation.

Yielding

crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal. To support this, you must add a block argument to your method signature, and use the yield keyword to call the block.

See notes on how to define a Proc type in Crystal here

require'crystalruby'crystallizedefyielder_cr(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendexpose_to_crystaldefyielder_rb(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendcrystallizedefinvoke_yielder_rb(a: Int32,b: Int32)yielder_rb(a,b)do |sum|
putssumendendyielder_cr(10,20){|sum| putssum}#=> 30invoke_yielder_rb(50,50)#=> 100

Exceptions

Exceptions thrown in Crystal code can be caught in Ruby.

Using shards

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

  • run shards install for you
  • require the specified shard upon compilation.

If your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.

Wrapping Crystal code in Ruby

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:int32do |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 + 1enddefconvert_to_i_and_add_and_succ(a: :int32,b: :int32)a + bendputsconvert_to_i_and_add_and_succ("1","2")

Inline Chunks

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.

  1. Start our toy project
mkdir crystalredis
cd crystalredis
bundle init
  1. Add dependencies to our Gemfile and run bundle install
# frozen_string_literal: truesource"https://rubygems.org"gem'crystalruby'# Let's see if performance is comparable to that of the redis gem.gem'benchmark-ips'gem'redis'
  1. Write our Redis client
# Filename: crystalredis.rbrequire'crystalruby'moduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendend
  1. Compile and benchmark our new module in Ruby
# Filename: benchmark.rb# Let's compare the performance of our CrystalRedis module to the Ruby Redis gemrequire'crystalruby'require'redis'require'benchmark/ips'require'debug'# For a high IPS single-threaded program, we can set the single_thread_mode to true for faster# FFI interopCrystalRuby.configuredo |config|
config.single_thread_mode=trueendmoduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendendBenchmark.ipsdo |x|
rbredis=Redis.newx.report(:crredis)doCrystalRedis.set("hello","world")CrystalRedis.get("hello")endx.report(:rbredis)dorbredis.set("hello","world")rbredis.get("hello")endend
  1. Run the benchmark
$ bundle exec ruby benchmark.rb

Release Builds

You can control whether crystalruby builds in debug or release mode by setting following config option

CrystalRuby.configuredo |config|
config.debug=falseend

By 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

  • Preloads all files Ruby code with embedded crystal
  • Forces compilation.

E.g.

# E.g. crystalruby_build.rbrequire"crystalruby"CrystalRuby.configuredo |config|
config.debug=falseendrequire_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.

Concurrency

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.

moduleSleepercrystallizedefsleep_syncsleep2.secondsendcrystallizeasync: truedefsleep_asyncsleep2.secondsendend
5.times.map{Thread.new{Sleeper.sleep_sync}}.each(&:join)# Will take 10 seconds5.times.map{Thread.new{Sleeper.sleep_async}}.each(&:join)# Will take 2 seconds (the sleeps are processed concurrently)

Reactor performance

There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 microseconds per call. For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2) then the overhead of the reactor can become significant.

In this case you can use the crystalruby in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that all calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.

CrystalRuby.configuredo |config|
config.single_thread_mode=trueend

Live Reloading

crystalruby supports live reloading of Crystal code. It will intelligently recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.

Multi-library support

Large Crystal projects are known to have long compile times. To mitigate this, crystalruby supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystal code within the project. To indicate which library a piece of embedded Crystal code belongs to, you can use the lib option in the crystallize and crystal methods. If the lib option is not provided, the code will be compiled into the default library (simply named crystalruby).

moduleFoocrystallizelib: "foo"defbarputs"Hello from Foo"endcrystallib: "foo"doREDIS=Redis.newendend

Naturally, Crystal methods must reside in the same library to natively interact. Cross library interaction can be facilitated via Ruby code.

Troubleshooting

In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.

To do this execute:

bundle exec crystalruby clean

Design Goals

crystalruby's primary purpose is to provide ergonomic access to Crystal from Ruby, over FFI. For simple usage, advanced knowledge of Crystal should not be required.

However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.

It should support escape hatches to allow it to coexist with code that performs a more direct FFI integration to implement advanced functionality not supported by crystalruby.

The library is currently in its infancy.

Installation

To get started, add this line to your application's Gemfile:

gem'crystalruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crystalruby

crystalruby supports some basic configuration options, which can be specified inside a crystalruby.yaml file in the root of your project. You can run crystalruby init to generate a configuration file with sane defaults.

$ crystalruby init
crystal_src_dir: "./crystalruby"crystal_codegen_dir: "generated"crystal_main_file: "main.cr"crystal_lib_name: "crlib"crystal_codegen_dir: "generated"debug: true

Alternatively, these can be set programmatically, e.g:

CrystalRuby.configuredo |config|
config.crystal_src_dir="./crystalruby"config.crystal_codegen_dir="generated"config.crystal_missing_ignore=falseconfig.debug=trueconfig.verbose=falseconfig.colorize_log_output=falseconfig.log_level=:infoend

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Embed Crystal code directly in Ruby

Resources

Code of conduct

Stars

653 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

logo

crystalruby

GEM Version
GEM Downloads

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.crystallizedefadd(a: Int32,b: Int32,returns: Int32)a + bend# This method is run in Crystal, not Ruby!putsadd(1,2)# => 3

With 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.

require'crystalruby'require'benchmark'crystallize:int32defcount_primes_upto_cr(n: Int32)(2..n).each.countdo |i|
is_prime=true(2..Math.isqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendenddefcount_primes_upto_rb(n)(2..n).each.countdo |i|
is_prime=true(2..Integer.sqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendendputsBenchmark.realtime{count_primes_upto_rb(1_000_000)}putsBenchmark.realtime{count_primes_upto_cr(1_000_000)}
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'moduleCacheshard:redis,github: 'jgaskins/redis'crystallize:stringdefredis_get(key: String)rds=Redis::Client.newvalue=rds.get(key).to_sendcrystallize:stringdefredis_set_and_return(key: String,value: String)redis=Redis::Client.newredis.set(key,value)Cache.redis_get(key)endendCache.redis_set_and_return('test','abc')putsCache.redis_get('test')
$ abc

Syntax

To 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.

Method Signatures

Parameter types are defined using kwarg syntax, with the type as the value. E.g.

deffoo(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 Int32crystallize->{Int32}defreturns_int323end# You can use the symbol shortcode for primitive typescrystallize:int32defreturns_int323end# Define the return type directly using the `returns` kwargcrystallizedefreturns_int32(returns: Int32)3end

Ruby Compatible Method Bodies

Where 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:intdefadd(a: :int,b: :int)puts"Adding #{a} and #{b}"a + bend

Crystal-only Syntax

Some Crystal syntax is not valid Ruby, for methods of this form, we need to define our functions using the raw: true option

crystallizeraw: truedefadd(a: :int,b: :int)<<~CRYSTAL c = 0_u64 a + b + c CRYSTALend

Upgrading from version 0.2.x

Change in type signatures

In version 0.2.x, argument and return types were passed to the crystallize method using a different syntax:

# V <= 0.2.xcrystallize[arg1: :arg1_type,arg2: :arg2_type]=>:return_typedeffoo(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.xcrystallize:return_typedeffoo(arg1: :arg1_type,arg2: :arg2_type)# OR use the `returns` kwargcrystallizedeffoo(arg1: :arg1_type,arg2: :arg2_type,returns: :return_type)

Getting Started

The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.

# crystalrubytest.rbrequire'bundler/inline'gemfiledosource'https://rubygems.org'gem'crystalruby'endrequire'crystalruby'crystallize:intdefadd(a: :int,b: :int)a + bendputsadd(1,2)

Types

Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.

Supported Types

  • UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
  • Time
  • Symbol
  • Nil
  • Bool
  • Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
  • Proc

Primitive types short-hands

  • :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
  • :int :uint :int32 :uint32 :long :ulong :int64 :uint64
  • :long_long :ulong_long :float :double :bool
  • :void :pointer :string

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'crystallizedefcomplex_argument_types(a: Int64 | Float64 | Nil,b: String | Array(Bool))puts"Got #{a} and #{b}"endcrystallizedefcomplex_return_type(returns: Int32 | String | Hash(String,Array(NamedTuple(hello: Int32)) | Time))return{"hello"=>[{hello: 1,},],"world"=>Time.utc}endcomplex_argument_types(10,"Hello")putscomplex_return_type()

Type signatures validations are applied to both arguments and return types.

[1]pry(main)> complex_argument_types(nil,"test")Gotandtest=>nil[2]pry(main)> complex_argument_types(88,[true,false,true])Got88and[true,false,true]=>nil[3]pry(main)> complex_argument_types(88,[true,false,88])ArgumentError: ExpectedBoolbutwasIntatline1,column15fromcrystalruby.rb:303:in `blockin compile!'

Reference Types

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)}crystallizedefmethod_with_named_types(a: IntArrOrBoolArr,returns: IntArrOrBoolArr)returnaend# 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 referenceint_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)}crystallizedefarray_doubler(a: IntArray)a.map!{ |x| x * 2}enddefarray_doubler_rb(a)a.map!{ |x| x * 2}endbig_array=Array.new(1_000_000){rand(100)}big_int_array=IntArray.new(big_array)Benchmark.bmdo |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)}end

Shared Instances

You 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'classPerson < CRType{NamedTuple(name: String,age: Int32)}defgreet_rb"Hello from Ruby. My name is #{self.name.value}"endcrystallize:stringdefgreet_cr"Hello from Crystal, My name is #{self.name.value}"endendperson=Person.new(name: "Bob",age: 30)putsperson.greet_rbperson.name="Alice"putsperson.greet_cr

Calling Ruby from Crystal

You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with expose_to_crystal so that crystalruby can perform the appropriate type conversions.

require'crystalruby'moduleAdderexpose_to_crystal:int32defadd_rb(a: Int32,b: Int32)a + bendcrystallize:int32defadd_crystal(a: Int32,b: Int32)returnadd_rb(a,b)endendputsAdder.add_crystal(1,2)

Kemal

Here's a more realistic example of where you could call Ruby from Crystal. We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing us to combine the raw speed of Kemal, with the flexibility of Ruby.

require'crystalruby'shard:kemal,github: 'kemalcr/kemal'crystallizeasync: truedefstart_serverKemal.run(3000,[""])endexpose_to_crystaldefreturn_ruby_response(returns: String)"Hello World! #{Random.rand(0..100)}"endcrystaldoget"/kemal_rb"doreturn_ruby_responseendget"/kemal_cr"do"Hello World! #{Random.rand(0..100)}"endendstart_server

We could compare the above to an equivalent pure Ruby implementation using Sinatra.

require'sinatra'get'/sinatra_rb'do'Hello world!'end

and benchmark the two.

$ wrk -d 2 http://localhost:4567/kemal_rb
... Requests/sec: 23352.00
$ wrk -d 2 http://localhost:4567/kemal_cr
... Requests/sec: 35730.03
$ wrk -d 2 http://localhost:4567/sinatra_rb
... Requests/sec: 5300.67

Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation and almost 66% as fast as the pure Crystal implementation.

Yielding

crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal. To support this, you must add a block argument to your method signature, and use the yield keyword to call the block.

See notes on how to define a Proc type in Crystal here

require'crystalruby'crystallizedefyielder_cr(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendexpose_to_crystaldefyielder_rb(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendcrystallizedefinvoke_yielder_rb(a: Int32,b: Int32)yielder_rb(a,b)do |sum|
putssumendendyielder_cr(10,20){|sum| putssum}#=> 30invoke_yielder_rb(50,50)#=> 100

Exceptions

Exceptions thrown in Crystal code can be caught in Ruby.

Using shards

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

  • run shards install for you
  • require the specified shard upon compilation.

If your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.

Wrapping Crystal code in Ruby

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:int32do |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 + 1enddefconvert_to_i_and_add_and_succ(a: :int32,b: :int32)a + bendputsconvert_to_i_and_add_and_succ("1","2")

Inline Chunks

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.

  1. Start our toy project
mkdir crystalredis
cd crystalredis
bundle init
  1. Add dependencies to our Gemfile and run bundle install
# frozen_string_literal: truesource"https://rubygems.org"gem'crystalruby'# Let's see if performance is comparable to that of the redis gem.gem'benchmark-ips'gem'redis'
  1. Write our Redis client
# Filename: crystalredis.rbrequire'crystalruby'moduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendend
  1. Compile and benchmark our new module in Ruby
# Filename: benchmark.rb# Let's compare the performance of our CrystalRedis module to the Ruby Redis gemrequire'crystalruby'require'redis'require'benchmark/ips'require'debug'# For a high IPS single-threaded program, we can set the single_thread_mode to true for faster# FFI interopCrystalRuby.configuredo |config|
config.single_thread_mode=trueendmoduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendendBenchmark.ipsdo |x|
rbredis=Redis.newx.report(:crredis)doCrystalRedis.set("hello","world")CrystalRedis.get("hello")endx.report(:rbredis)dorbredis.set("hello","world")rbredis.get("hello")endend
  1. Run the benchmark
$ bundle exec ruby benchmark.rb

Release Builds

You can control whether crystalruby builds in debug or release mode by setting following config option

CrystalRuby.configuredo |config|
config.debug=falseend

By 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

  • Preloads all files Ruby code with embedded crystal
  • Forces compilation.

E.g.

# E.g. crystalruby_build.rbrequire"crystalruby"CrystalRuby.configuredo |config|
config.debug=falseendrequire_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.

Concurrency

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.

moduleSleepercrystallizedefsleep_syncsleep2.secondsendcrystallizeasync: truedefsleep_asyncsleep2.secondsendend
5.times.map{Thread.new{Sleeper.sleep_sync}}.each(&:join)# Will take 10 seconds5.times.map{Thread.new{Sleeper.sleep_async}}.each(&:join)# Will take 2 seconds (the sleeps are processed concurrently)

Reactor performance

There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 microseconds per call. For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2) then the overhead of the reactor can become significant.

In this case you can use the crystalruby in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that all calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.

CrystalRuby.configuredo |config|
config.single_thread_mode=trueend

Live Reloading

crystalruby supports live reloading of Crystal code. It will intelligently recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.

Multi-library support

Large Crystal projects are known to have long compile times. To mitigate this, crystalruby supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystal code within the project. To indicate which library a piece of embedded Crystal code belongs to, you can use the lib option in the crystallize and crystal methods. If the lib option is not provided, the code will be compiled into the default library (simply named crystalruby).

moduleFoocrystallizelib: "foo"defbarputs"Hello from Foo"endcrystallib: "foo"doREDIS=Redis.newendend

Naturally, Crystal methods must reside in the same library to natively interact. Cross library interaction can be facilitated via Ruby code.

Troubleshooting

In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.

To do this execute:

bundle exec crystalruby clean

Design Goals

crystalruby's primary purpose is to provide ergonomic access to Crystal from Ruby, over FFI. For simple usage, advanced knowledge of Crystal should not be required.

However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.

It should support escape hatches to allow it to coexist with code that performs a more direct FFI integration to implement advanced functionality not supported by crystalruby.

The library is currently in its infancy.

Installation

To get started, add this line to your application's Gemfile:

gem'crystalruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crystalruby

crystalruby supports some basic configuration options, which can be specified inside a crystalruby.yaml file in the root of your project. You can run crystalruby init to generate a configuration file with sane defaults.

$ crystalruby init
crystal_src_dir: "./crystalruby"crystal_codegen_dir: "generated"crystal_main_file: "main.cr"crystal_lib_name: "crlib"crystal_codegen_dir: "generated"debug: true

Alternatively, these can be set programmatically, e.g:

CrystalRuby.configuredo |config|
config.crystal_src_dir="./crystalruby"config.crystal_codegen_dir="generated"config.crystal_missing_ignore=falseconfig.debug=trueconfig.verbose=falseconfig.colorize_log_output=falseconfig.log_level=:infoend

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Embed Crystal code directly in Ruby

Resources

Code of conduct

Stars

653 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

logo

crystalruby

GEM Version
GEM Downloads

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.crystallizedefadd(a: Int32,b: Int32,returns: Int32)a + bend# This method is run in Crystal, not Ruby!putsadd(1,2)# => 3

With 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.

require'crystalruby'require'benchmark'crystallize:int32defcount_primes_upto_cr(n: Int32)(2..n).each.countdo |i|
is_prime=true(2..Math.isqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendenddefcount_primes_upto_rb(n)(2..n).each.countdo |i|
is_prime=true(2..Integer.sqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendendputsBenchmark.realtime{count_primes_upto_rb(1_000_000)}putsBenchmark.realtime{count_primes_upto_cr(1_000_000)}
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'moduleCacheshard:redis,github: 'jgaskins/redis'crystallize:stringdefredis_get(key: String)rds=Redis::Client.newvalue=rds.get(key).to_sendcrystallize:stringdefredis_set_and_return(key: String,value: String)redis=Redis::Client.newredis.set(key,value)Cache.redis_get(key)endendCache.redis_set_and_return('test','abc')putsCache.redis_get('test')
$ abc

Syntax

To 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.

Method Signatures

Parameter types are defined using kwarg syntax, with the type as the value. E.g.

deffoo(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 Int32crystallize->{Int32}defreturns_int323end# You can use the symbol shortcode for primitive typescrystallize:int32defreturns_int323end# Define the return type directly using the `returns` kwargcrystallizedefreturns_int32(returns: Int32)3end

Ruby Compatible Method Bodies

Where 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:intdefadd(a: :int,b: :int)puts"Adding #{a} and #{b}"a + bend

Crystal-only Syntax

Some Crystal syntax is not valid Ruby, for methods of this form, we need to define our functions using the raw: true option

crystallizeraw: truedefadd(a: :int,b: :int)<<~CRYSTAL c = 0_u64 a + b + c CRYSTALend

Upgrading from version 0.2.x

Change in type signatures

In version 0.2.x, argument and return types were passed to the crystallize method using a different syntax:

# V <= 0.2.xcrystallize[arg1: :arg1_type,arg2: :arg2_type]=>:return_typedeffoo(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.xcrystallize:return_typedeffoo(arg1: :arg1_type,arg2: :arg2_type)# OR use the `returns` kwargcrystallizedeffoo(arg1: :arg1_type,arg2: :arg2_type,returns: :return_type)

Getting Started

The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.

# crystalrubytest.rbrequire'bundler/inline'gemfiledosource'https://rubygems.org'gem'crystalruby'endrequire'crystalruby'crystallize:intdefadd(a: :int,b: :int)a + bendputsadd(1,2)

Types

Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.

Supported Types

  • UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
  • Time
  • Symbol
  • Nil
  • Bool
  • Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
  • Proc

Primitive types short-hands

  • :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
  • :int :uint :int32 :uint32 :long :ulong :int64 :uint64
  • :long_long :ulong_long :float :double :bool
  • :void :pointer :string

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'crystallizedefcomplex_argument_types(a: Int64 | Float64 | Nil,b: String | Array(Bool))puts"Got #{a} and #{b}"endcrystallizedefcomplex_return_type(returns: Int32 | String | Hash(String,Array(NamedTuple(hello: Int32)) | Time))return{"hello"=>[{hello: 1,},],"world"=>Time.utc}endcomplex_argument_types(10,"Hello")putscomplex_return_type()

Type signatures validations are applied to both arguments and return types.

[1]pry(main)> complex_argument_types(nil,"test")Gotandtest=>nil[2]pry(main)> complex_argument_types(88,[true,false,true])Got88and[true,false,true]=>nil[3]pry(main)> complex_argument_types(88,[true,false,88])ArgumentError: ExpectedBoolbutwasIntatline1,column15fromcrystalruby.rb:303:in `blockin compile!'

Reference Types

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)}crystallizedefmethod_with_named_types(a: IntArrOrBoolArr,returns: IntArrOrBoolArr)returnaend# 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 referenceint_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)}crystallizedefarray_doubler(a: IntArray)a.map!{ |x| x * 2}enddefarray_doubler_rb(a)a.map!{ |x| x * 2}endbig_array=Array.new(1_000_000){rand(100)}big_int_array=IntArray.new(big_array)Benchmark.bmdo |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)}end

Shared Instances

You 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'classPerson < CRType{NamedTuple(name: String,age: Int32)}defgreet_rb"Hello from Ruby. My name is #{self.name.value}"endcrystallize:stringdefgreet_cr"Hello from Crystal, My name is #{self.name.value}"endendperson=Person.new(name: "Bob",age: 30)putsperson.greet_rbperson.name="Alice"putsperson.greet_cr

Calling Ruby from Crystal

You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with expose_to_crystal so that crystalruby can perform the appropriate type conversions.

require'crystalruby'moduleAdderexpose_to_crystal:int32defadd_rb(a: Int32,b: Int32)a + bendcrystallize:int32defadd_crystal(a: Int32,b: Int32)returnadd_rb(a,b)endendputsAdder.add_crystal(1,2)

Kemal

Here's a more realistic example of where you could call Ruby from Crystal. We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing us to combine the raw speed of Kemal, with the flexibility of Ruby.

require'crystalruby'shard:kemal,github: 'kemalcr/kemal'crystallizeasync: truedefstart_serverKemal.run(3000,[""])endexpose_to_crystaldefreturn_ruby_response(returns: String)"Hello World! #{Random.rand(0..100)}"endcrystaldoget"/kemal_rb"doreturn_ruby_responseendget"/kemal_cr"do"Hello World! #{Random.rand(0..100)}"endendstart_server

We could compare the above to an equivalent pure Ruby implementation using Sinatra.

require'sinatra'get'/sinatra_rb'do'Hello world!'end

and benchmark the two.

$ wrk -d 2 http://localhost:4567/kemal_rb
... Requests/sec: 23352.00
$ wrk -d 2 http://localhost:4567/kemal_cr
... Requests/sec: 35730.03
$ wrk -d 2 http://localhost:4567/sinatra_rb
... Requests/sec: 5300.67

Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation and almost 66% as fast as the pure Crystal implementation.

Yielding

crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal. To support this, you must add a block argument to your method signature, and use the yield keyword to call the block.

See notes on how to define a Proc type in Crystal here

require'crystalruby'crystallizedefyielder_cr(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendexpose_to_crystaldefyielder_rb(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendcrystallizedefinvoke_yielder_rb(a: Int32,b: Int32)yielder_rb(a,b)do |sum|
putssumendendyielder_cr(10,20){|sum| putssum}#=> 30invoke_yielder_rb(50,50)#=> 100

Exceptions

Exceptions thrown in Crystal code can be caught in Ruby.

Using shards

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

  • run shards install for you
  • require the specified shard upon compilation.

If your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.

Wrapping Crystal code in Ruby

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:int32do |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 + 1enddefconvert_to_i_and_add_and_succ(a: :int32,b: :int32)a + bendputsconvert_to_i_and_add_and_succ("1","2")

Inline Chunks

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.

  1. Start our toy project
mkdir crystalredis
cd crystalredis
bundle init
  1. Add dependencies to our Gemfile and run bundle install
# frozen_string_literal: truesource"https://rubygems.org"gem'crystalruby'# Let's see if performance is comparable to that of the redis gem.gem'benchmark-ips'gem'redis'
  1. Write our Redis client
# Filename: crystalredis.rbrequire'crystalruby'moduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendend
  1. Compile and benchmark our new module in Ruby
# Filename: benchmark.rb# Let's compare the performance of our CrystalRedis module to the Ruby Redis gemrequire'crystalruby'require'redis'require'benchmark/ips'require'debug'# For a high IPS single-threaded program, we can set the single_thread_mode to true for faster# FFI interopCrystalRuby.configuredo |config|
config.single_thread_mode=trueendmoduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendendBenchmark.ipsdo |x|
rbredis=Redis.newx.report(:crredis)doCrystalRedis.set("hello","world")CrystalRedis.get("hello")endx.report(:rbredis)dorbredis.set("hello","world")rbredis.get("hello")endend
  1. Run the benchmark
$ bundle exec ruby benchmark.rb

Release Builds

You can control whether crystalruby builds in debug or release mode by setting following config option

CrystalRuby.configuredo |config|
config.debug=falseend

By 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

  • Preloads all files Ruby code with embedded crystal
  • Forces compilation.

E.g.

# E.g. crystalruby_build.rbrequire"crystalruby"CrystalRuby.configuredo |config|
config.debug=falseendrequire_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.

Concurrency

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.

moduleSleepercrystallizedefsleep_syncsleep2.secondsendcrystallizeasync: truedefsleep_asyncsleep2.secondsendend
5.times.map{Thread.new{Sleeper.sleep_sync}}.each(&:join)# Will take 10 seconds5.times.map{Thread.new{Sleeper.sleep_async}}.each(&:join)# Will take 2 seconds (the sleeps are processed concurrently)

Reactor performance

There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 microseconds per call. For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2) then the overhead of the reactor can become significant.

In this case you can use the crystalruby in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that all calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.

CrystalRuby.configuredo |config|
config.single_thread_mode=trueend

Live Reloading

crystalruby supports live reloading of Crystal code. It will intelligently recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.

Multi-library support

Large Crystal projects are known to have long compile times. To mitigate this, crystalruby supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystal code within the project. To indicate which library a piece of embedded Crystal code belongs to, you can use the lib option in the crystallize and crystal methods. If the lib option is not provided, the code will be compiled into the default library (simply named crystalruby).

moduleFoocrystallizelib: "foo"defbarputs"Hello from Foo"endcrystallib: "foo"doREDIS=Redis.newendend

Naturally, Crystal methods must reside in the same library to natively interact. Cross library interaction can be facilitated via Ruby code.

Troubleshooting

In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.

To do this execute:

bundle exec crystalruby clean

Design Goals

crystalruby's primary purpose is to provide ergonomic access to Crystal from Ruby, over FFI. For simple usage, advanced knowledge of Crystal should not be required.

However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.

It should support escape hatches to allow it to coexist with code that performs a more direct FFI integration to implement advanced functionality not supported by crystalruby.

The library is currently in its infancy.

Installation

To get started, add this line to your application's Gemfile:

gem'crystalruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crystalruby

crystalruby supports some basic configuration options, which can be specified inside a crystalruby.yaml file in the root of your project. You can run crystalruby init to generate a configuration file with sane defaults.

$ crystalruby init
crystal_src_dir: "./crystalruby"crystal_codegen_dir: "generated"crystal_main_file: "main.cr"crystal_lib_name: "crlib"crystal_codegen_dir: "generated"debug: true

Alternatively, these can be set programmatically, e.g:

CrystalRuby.configuredo |config|
config.crystal_src_dir="./crystalruby"config.crystal_codegen_dir="generated"config.crystal_missing_ignore=falseconfig.debug=trueconfig.verbose=falseconfig.colorize_log_output=falseconfig.log_level=:infoend

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Embed Crystal code directly in Ruby

Resources

Code of conduct

Stars

653 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

logo

crystalruby

GEM Version
GEM Downloads

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.crystallizedefadd(a: Int32,b: Int32,returns: Int32)a + bend# This method is run in Crystal, not Ruby!putsadd(1,2)# => 3

With 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.

require'crystalruby'require'benchmark'crystallize:int32defcount_primes_upto_cr(n: Int32)(2..n).each.countdo |i|
is_prime=true(2..Math.isqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendenddefcount_primes_upto_rb(n)(2..n).each.countdo |i|
is_prime=true(2..Integer.sqrt(i)).eachdo |j|
ifi % j == 0is_prime=falsebreakendendis_primeendendputsBenchmark.realtime{count_primes_upto_rb(1_000_000)}putsBenchmark.realtime{count_primes_upto_cr(1_000_000)}
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'moduleCacheshard:redis,github: 'jgaskins/redis'crystallize:stringdefredis_get(key: String)rds=Redis::Client.newvalue=rds.get(key).to_sendcrystallize:stringdefredis_set_and_return(key: String,value: String)redis=Redis::Client.newredis.set(key,value)Cache.redis_get(key)endendCache.redis_set_and_return('test','abc')putsCache.redis_get('test')
$ abc

Syntax

To 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.

Method Signatures

Parameter types are defined using kwarg syntax, with the type as the value. E.g.

deffoo(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 Int32crystallize->{Int32}defreturns_int323end# You can use the symbol shortcode for primitive typescrystallize:int32defreturns_int323end# Define the return type directly using the `returns` kwargcrystallizedefreturns_int32(returns: Int32)3end

Ruby Compatible Method Bodies

Where 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:intdefadd(a: :int,b: :int)puts"Adding #{a} and #{b}"a + bend

Crystal-only Syntax

Some Crystal syntax is not valid Ruby, for methods of this form, we need to define our functions using the raw: true option

crystallizeraw: truedefadd(a: :int,b: :int)<<~CRYSTAL c = 0_u64 a + b + c CRYSTALend

Upgrading from version 0.2.x

Change in type signatures

In version 0.2.x, argument and return types were passed to the crystallize method using a different syntax:

# V <= 0.2.xcrystallize[arg1: :arg1_type,arg2: :arg2_type]=>:return_typedeffoo(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.xcrystallize:return_typedeffoo(arg1: :arg1_type,arg2: :arg2_type)# OR use the `returns` kwargcrystallizedeffoo(arg1: :arg1_type,arg2: :arg2_type,returns: :return_type)

Getting Started

The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.

# crystalrubytest.rbrequire'bundler/inline'gemfiledosource'https://rubygems.org'gem'crystalruby'endrequire'crystalruby'crystallize:intdefadd(a: :int,b: :int)a + bendputsadd(1,2)

Types

Most built-in Crystal Types are available. You can also use the :symbol short-hand for primitive types.

Supported Types

  • UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
  • Time
  • Symbol
  • Nil
  • Bool
  • Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
  • Proc

Primitive types short-hands

  • :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
  • :int :uint :int32 :uint32 :long :ulong :int64 :uint64
  • :long_long :ulong_long :float :double :bool
  • :void :pointer :string

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'crystallizedefcomplex_argument_types(a: Int64 | Float64 | Nil,b: String | Array(Bool))puts"Got #{a} and #{b}"endcrystallizedefcomplex_return_type(returns: Int32 | String | Hash(String,Array(NamedTuple(hello: Int32)) | Time))return{"hello"=>[{hello: 1,},],"world"=>Time.utc}endcomplex_argument_types(10,"Hello")putscomplex_return_type()

Type signatures validations are applied to both arguments and return types.

[1]pry(main)> complex_argument_types(nil,"test")Gotandtest=>nil[2]pry(main)> complex_argument_types(88,[true,false,true])Got88and[true,false,true]=>nil[3]pry(main)> complex_argument_types(88,[true,false,88])ArgumentError: ExpectedBoolbutwasIntatline1,column15fromcrystalruby.rb:303:in `blockin compile!'

Reference Types

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)}crystallizedefmethod_with_named_types(a: IntArrOrBoolArr,returns: IntArrOrBoolArr)returnaend# 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 referenceint_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)}crystallizedefarray_doubler(a: IntArray)a.map!{ |x| x * 2}enddefarray_doubler_rb(a)a.map!{ |x| x * 2}endbig_array=Array.new(1_000_000){rand(100)}big_int_array=IntArray.new(big_array)Benchmark.bmdo |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)}end

Shared Instances

You 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'classPerson < CRType{NamedTuple(name: String,age: Int32)}defgreet_rb"Hello from Ruby. My name is #{self.name.value}"endcrystallize:stringdefgreet_cr"Hello from Crystal, My name is #{self.name.value}"endendperson=Person.new(name: "Bob",age: 30)putsperson.greet_rbperson.name="Alice"putsperson.greet_cr

Calling Ruby from Crystal

You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with expose_to_crystal so that crystalruby can perform the appropriate type conversions.

require'crystalruby'moduleAdderexpose_to_crystal:int32defadd_rb(a: Int32,b: Int32)a + bendcrystallize:int32defadd_crystal(a: Int32,b: Int32)returnadd_rb(a,b)endendputsAdder.add_crystal(1,2)

Kemal

Here's a more realistic example of where you could call Ruby from Crystal. We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing us to combine the raw speed of Kemal, with the flexibility of Ruby.

require'crystalruby'shard:kemal,github: 'kemalcr/kemal'crystallizeasync: truedefstart_serverKemal.run(3000,[""])endexpose_to_crystaldefreturn_ruby_response(returns: String)"Hello World! #{Random.rand(0..100)}"endcrystaldoget"/kemal_rb"doreturn_ruby_responseendget"/kemal_cr"do"Hello World! #{Random.rand(0..100)}"endendstart_server

We could compare the above to an equivalent pure Ruby implementation using Sinatra.

require'sinatra'get'/sinatra_rb'do'Hello world!'end

and benchmark the two.

$ wrk -d 2 http://localhost:4567/kemal_rb
... Requests/sec: 23352.00
$ wrk -d 2 http://localhost:4567/kemal_cr
... Requests/sec: 35730.03
$ wrk -d 2 http://localhost:4567/sinatra_rb
... Requests/sec: 5300.67

Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation and almost 66% as fast as the pure Crystal implementation.

Yielding

crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal. To support this, you must add a block argument to your method signature, and use the yield keyword to call the block.

See notes on how to define a Proc type in Crystal here

require'crystalruby'crystallizedefyielder_cr(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendexpose_to_crystaldefyielder_rb(a: Int32,b: Int32,yield: Proc(Int32,Nil))yielda + bendcrystallizedefinvoke_yielder_rb(a: Int32,b: Int32)yielder_rb(a,b)do |sum|
putssumendendyielder_cr(10,20){|sum| putssum}#=> 30invoke_yielder_rb(50,50)#=> 100

Exceptions

Exceptions thrown in Crystal code can be caught in Ruby.

Using shards

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

  • run shards install for you
  • require the specified shard upon compilation.

If your shard file gets out of sync with your Ruby file, you can run crystalruby clean to reset your workspace to a clean state.

Wrapping Crystal code in Ruby

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:int32do |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 + 1enddefconvert_to_i_and_add_and_succ(a: :int32,b: :int32)a + bendputsconvert_to_i_and_add_and_succ("1","2")

Inline Chunks

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.

  1. Start our toy project
mkdir crystalredis
cd crystalredis
bundle init
  1. Add dependencies to our Gemfile and run bundle install
# frozen_string_literal: truesource"https://rubygems.org"gem'crystalruby'# Let's see if performance is comparable to that of the redis gem.gem'benchmark-ips'gem'redis'
  1. Write our Redis client
# Filename: crystalredis.rbrequire'crystalruby'moduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendend
  1. Compile and benchmark our new module in Ruby
# Filename: benchmark.rb# Let's compare the performance of our CrystalRedis module to the Ruby Redis gemrequire'crystalruby'require'redis'require'benchmark/ips'require'debug'# For a high IPS single-threaded program, we can set the single_thread_mode to true for faster# FFI interopCrystalRuby.configuredo |config|
config.single_thread_mode=trueendmoduleCrystalRedisshard:redis,github: 'stefanwille/crystal-redis'crystaldoCLIENT=Redis.newdefself.clientCLIENTendendcrystallizedefset(key: String,value: String)client.set(key,value)endcrystallize:stringdefget(key: String)client.get(key).to_sendendBenchmark.ipsdo |x|
rbredis=Redis.newx.report(:crredis)doCrystalRedis.set("hello","world")CrystalRedis.get("hello")endx.report(:rbredis)dorbredis.set("hello","world")rbredis.get("hello")endend
  1. Run the benchmark
$ bundle exec ruby benchmark.rb

Release Builds

You can control whether crystalruby builds in debug or release mode by setting following config option

CrystalRuby.configuredo |config|
config.debug=falseend

By 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

  • Preloads all files Ruby code with embedded crystal
  • Forces compilation.

E.g.

# E.g. crystalruby_build.rbrequire"crystalruby"CrystalRuby.configuredo |config|
config.debug=falseendrequire_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.

Concurrency

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.

moduleSleepercrystallizedefsleep_syncsleep2.secondsendcrystallizeasync: truedefsleep_asyncsleep2.secondsendend
5.times.map{Thread.new{Sleeper.sleep_sync}}.each(&:join)# Will take 10 seconds5.times.map{Thread.new{Sleeper.sleep_async}}.each(&:join)# Will take 2 seconds (the sleeps are processed concurrently)

Reactor performance

There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 microseconds per call. For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2) then the overhead of the reactor can become significant.

In this case you can use the crystalruby in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that all calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.

CrystalRuby.configuredo |config|
config.single_thread_mode=trueend

Live Reloading

crystalruby supports live reloading of Crystal code. It will intelligently recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.

Multi-library support

Large Crystal projects are known to have long compile times. To mitigate this, crystalruby supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystal code within the project. To indicate which library a piece of embedded Crystal code belongs to, you can use the lib option in the crystallize and crystal methods. If the lib option is not provided, the code will be compiled into the default library (simply named crystalruby).

moduleFoocrystallizelib: "foo"defbarputs"Hello from Foo"endcrystallib: "foo"doREDIS=Redis.newendend

Naturally, Crystal methods must reside in the same library to natively interact. Cross library interaction can be facilitated via Ruby code.

Troubleshooting

In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.

To do this execute:

bundle exec crystalruby clean

Design Goals

crystalruby's primary purpose is to provide ergonomic access to Crystal from Ruby, over FFI. For simple usage, advanced knowledge of Crystal should not be required.

However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.

It should support escape hatches to allow it to coexist with code that performs a more direct FFI integration to implement advanced functionality not supported by crystalruby.

The library is currently in its infancy.

Installation

To get started, add this line to your application's Gemfile:

gem'crystalruby'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crystalruby

crystalruby supports some basic configuration options, which can be specified inside a crystalruby.yaml file in the root of your project. You can run crystalruby init to generate a configuration file with sane defaults.

$ crystalruby init
crystal_src_dir: "./crystalruby"crystal_codegen_dir: "generated"crystal_main_file: "main.cr"crystal_lib_name: "crlib"crystal_codegen_dir: "generated"debug: true

Alternatively, these can be set programmatically, e.g:

CrystalRuby.configuredo |config|
config.crystal_src_dir="./crystalruby"config.crystal_codegen_dir="generated"config.crystal_missing_ignore=falseconfig.debug=trueconfig.verbose=falseconfig.colorize_log_output=falseconfig.log_level=:infoend

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

Embed Crystal code directly in Ruby

Resources

Code of conduct

Stars

653 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages