crystalruby 0.2.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/Dockerfile +23 -2
  4. data/README.md +395 -198
  5. data/Rakefile +4 -3
  6. data/crystalruby.gemspec +2 -2
  7. data/examples/adder/adder.rb +1 -1
  8. data/exe/crystalruby +1 -0
  9. data/lib/crystalruby/adapter.rb +143 -73
  10. data/lib/crystalruby/arc_mutex.rb +47 -0
  11. data/lib/crystalruby/compilation.rb +32 -3
  12. data/lib/crystalruby/config.rb +41 -37
  13. data/lib/crystalruby/function.rb +216 -73
  14. data/lib/crystalruby/library.rb +157 -51
  15. data/lib/crystalruby/reactor.rb +63 -44
  16. data/lib/crystalruby/source_reader.rb +92 -0
  17. data/lib/crystalruby/template.rb +16 -5
  18. data/lib/crystalruby/templates/function.cr +11 -10
  19. data/lib/crystalruby/templates/index.cr +53 -66
  20. data/lib/crystalruby/templates/inline_chunk.cr +1 -1
  21. data/lib/crystalruby/templates/ruby_interface.cr +34 -0
  22. data/lib/crystalruby/templates/top_level_function.cr +62 -0
  23. data/lib/crystalruby/templates/top_level_ruby_interface.cr +33 -0
  24. data/lib/crystalruby/typebuilder.rb +11 -55
  25. data/lib/crystalruby/typemaps.rb +92 -67
  26. data/lib/crystalruby/types/concerns/allocator.rb +80 -0
  27. data/lib/crystalruby/types/fixed_width/named_tuple.cr +80 -0
  28. data/lib/crystalruby/types/fixed_width/named_tuple.rb +86 -0
  29. data/lib/crystalruby/types/fixed_width/proc.cr +45 -0
  30. data/lib/crystalruby/types/fixed_width/proc.rb +79 -0
  31. data/lib/crystalruby/types/fixed_width/tagged_union.cr +53 -0
  32. data/lib/crystalruby/types/fixed_width/tagged_union.rb +113 -0
  33. data/lib/crystalruby/types/fixed_width/tuple.cr +82 -0
  34. data/lib/crystalruby/types/fixed_width/tuple.rb +92 -0
  35. data/lib/crystalruby/types/fixed_width.cr +138 -0
  36. data/lib/crystalruby/types/fixed_width.rb +205 -0
  37. data/lib/crystalruby/types/primitive.cr +21 -0
  38. data/lib/crystalruby/types/primitive.rb +117 -0
  39. data/lib/crystalruby/types/primitive_types/bool.cr +34 -0
  40. data/lib/crystalruby/types/primitive_types/bool.rb +11 -0
  41. data/lib/crystalruby/types/primitive_types/nil.cr +35 -0
  42. data/lib/crystalruby/types/primitive_types/nil.rb +16 -0
  43. data/lib/crystalruby/types/primitive_types/numbers.cr +37 -0
  44. data/lib/crystalruby/types/primitive_types/numbers.rb +28 -0
  45. data/lib/crystalruby/types/primitive_types/symbol.cr +55 -0
  46. data/lib/crystalruby/types/primitive_types/symbol.rb +35 -0
  47. data/lib/crystalruby/types/primitive_types/time.cr +35 -0
  48. data/lib/crystalruby/types/primitive_types/time.rb +25 -0
  49. data/lib/crystalruby/types/type.cr +64 -0
  50. data/lib/crystalruby/types/type.rb +249 -30
  51. data/lib/crystalruby/types/variable_width/array.cr +74 -0
  52. data/lib/crystalruby/types/variable_width/array.rb +88 -0
  53. data/lib/crystalruby/types/variable_width/hash.cr +146 -0
  54. data/lib/crystalruby/types/variable_width/hash.rb +117 -0
  55. data/lib/crystalruby/types/variable_width/string.cr +36 -0
  56. data/lib/crystalruby/types/variable_width/string.rb +18 -0
  57. data/lib/crystalruby/types/variable_width.cr +23 -0
  58. data/lib/crystalruby/types/variable_width.rb +46 -0
  59. data/lib/crystalruby/types.rb +32 -13
  60. data/lib/crystalruby/version.rb +2 -2
  61. data/lib/crystalruby.rb +13 -6
  62. metadata +42 -22
  63. data/lib/crystalruby/types/array.rb +0 -15
  64. data/lib/crystalruby/types/bool.rb +0 -3
  65. data/lib/crystalruby/types/hash.rb +0 -17
  66. data/lib/crystalruby/types/named_tuple.rb +0 -28
  67. data/lib/crystalruby/types/nil.rb +0 -3
  68. data/lib/crystalruby/types/numbers.rb +0 -5
  69. data/lib/crystalruby/types/string.rb +0 -3
  70. data/lib/crystalruby/types/symbol.rb +0 -3
  71. data/lib/crystalruby/types/time.rb +0 -8
  72. data/lib/crystalruby/types/tuple.rb +0 -17
  73. data/lib/crystalruby/types/type_serializer/json.rb +0 -41
  74. data/lib/crystalruby/types/type_serializer.rb +0 -37
  75. data/lib/crystalruby/types/typedef.rb +0 -57
  76. data/lib/crystalruby/types/union_type.rb +0 -43
  77. data/lib/module.rb +0 -3
data/README.md CHANGED
@@ -15,91 +15,91 @@
15
15
  </tr>
16
16
  </table>
17
17
 
18
- `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.
18
+ `crystalruby` is a gem that allows you to write Crystal code, inlined in Ruby.
19
+ All you need is a modern crystal compiler installed on your system.
19
20
 
20
21
  You can then turn simple methods into Crystal methods as easily as demonstrated below:
21
22
 
22
23
  ```ruby
23
24
  require 'crystalruby'
24
25
 
25
- module MyTestModule
26
- # The below method will be replaced by a compiled Crystal version
27
- # linked using FFI.
28
- crystalize [a: :int, b: :int] => :int
29
- def add(a, b)
30
- a + b
31
- end
26
+ # The below method will be replaced by a compiled Crystal version
27
+ # linked using FFI.
28
+
29
+ crystallize
30
+ def add(a: Int32, b: Int32, returns: Int32)
31
+ a + b
32
32
  end
33
33
 
34
34
  # This method is run in Crystal, not Ruby!
35
- MyTestModule.add(1, 2) # => 3
35
+ puts add(1, 2) # => 3
36
36
  ```
37
37
 
38
- With as small a change as this, you should be able to see a significant increase in performance for some Ruby code.
38
+ 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.
39
39
  E.g.
40
40
 
41
41
  ```ruby
42
-
43
42
  require 'crystalruby'
44
43
  require 'benchmark'
45
44
 
46
- module PrimeCounter
47
- crystalize [n: :int32] => :int32
48
- def count_primes_upto_cr(n)
49
- (2..n).each.count do |i|
50
- is_prime = true
51
- (2..Math.sqrt(i).to_i).each do |j|
52
- if i % j == 0
53
- is_prime = false
54
- break
55
- end
45
+ crystallize :int32
46
+ def count_primes_upto_cr(n: Int32)
47
+ (2..n).each.count do |i|
48
+ is_prime = true
49
+ (2..Math.sqrt(i).to_i).each do |j|
50
+ if i % j == 0
51
+ is_prime = false
52
+ break
56
53
  end
57
- is_prime
58
54
  end
55
+ is_prime
59
56
  end
57
+ end
60
58
 
61
- module_function
62
-
63
- def count_primes_upto_rb(n)
64
- (2..n).each.count do |i|
65
- is_prime = true
66
- (2..Math.sqrt(i).to_i).each do |j|
67
- if i % j == 0
68
- is_prime = false
69
- break
70
- end
59
+ def count_primes_upto_rb(n)
60
+ (2..n).each.count do |i|
61
+ is_prime = true
62
+ (2..Math.sqrt(i).to_i).each do |j|
63
+ if i % j == 0
64
+ is_prime = false
65
+ break
71
66
  end
72
- is_prime
73
67
  end
68
+ is_prime
74
69
  end
75
70
  end
76
71
 
77
- include PrimeCounter
78
- puts(Benchmark.realtime { count_primes_upto_rb(1000_000) })
79
- puts(Benchmark.realtime { count_primes_upto_cr(1000_000) })
72
+ puts Benchmark.realtime { count_primes_upto_rb(1_000_000) }
73
+ puts Benchmark.realtime { count_primes_upto_cr(1_000_000) }
80
74
  ```
81
75
 
82
76
  ```bash
83
- 2.8195170001126826 # Ruby
84
- 0.3402599999681115 # Crystal
77
+ 3.04239400010556 # Ruby
78
+ 0.06029000016860 # Crystal (50x faster)
85
79
  ```
86
80
 
87
- _Note_: The first run of the Crystal code will be slower, as it needs to compile the code first. The subsequent runs will be much faster.
81
+ _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.
88
82
 
89
83
  You can call embedded crystal code, from within other embedded crystal code.
84
+ The below crystallized method `redis_set_and_return` calls the `redis_get` method, which is also crystallized.
85
+ Note the use of the shard command to define the Redis shard dependency of the crystallized code.
90
86
  E.g.
91
87
 
92
88
  ```ruby
89
+ require 'crystalruby'
90
+
93
91
  module Cache
94
92
 
95
- crystalize [key: :string] => :string
96
- def redis_get(key)
93
+ shard :redis, github: 'jgaskins/redis'
94
+
95
+ crystallize :string
96
+ def redis_get(key: String)
97
97
  rds = Redis::Client.new
98
98
  value = rds.get(key).to_s
99
99
  end
100
100
 
101
- crystalize [key: :string, value: :string] => :string
102
- def redis_set_and_return(key, value)
101
+ crystallize :string
102
+ def redis_set_and_return(key: String, value: String)
103
103
  redis = Redis::Client.new
104
104
  redis.set(key, value)
105
105
  Cache.redis_get(key)
@@ -115,29 +115,61 @@ $ abc
115
115
 
116
116
  ## Syntax
117
117
 
118
- ### Ruby Compatible
118
+ To define a method that will be compiled as Crystal, you can use the `crystallize` method.
119
+ You must also provide types, for the parameters and return type.
120
+
121
+ ### Method Signatures
122
+ Parameter types are defined using kwarg syntax, with the type as the value.
123
+ E.g.
124
+ ```ruby
125
+ def foo(a: Int32, b: Array(Int), c: String)
126
+ ```
127
+
128
+ Return types are specified using either a lambda, returning the type, as the first argument to the crystallize method, or the special `returns` kwarg.
129
+
130
+ E.g.
131
+
132
+ ```ruby
133
+ # Returns an Int32
134
+ crystallize ->{ Int32 }
135
+ def returns_int32
136
+ 3
137
+ end
138
+
139
+ # You can use the symbol shortcode for primitive types
140
+ crystallize :int32
141
+ def returns_int32
142
+ 3
143
+ end
119
144
 
120
- Where the Crystal syntax is also valid Ruby syntax, you can just write Ruby.
145
+ # Define the return type directly using the `returns` kwarg
146
+ crystallize
147
+ def returns_int32(returns: Int32)
148
+ 3
149
+ end
150
+ ```
151
+
152
+ ### Ruby Compatible Method Bodies
153
+ Where the Crystal syntax of the method body is also valid Ruby syntax, you can just write Ruby.
121
154
  It'll be compiled as Crystal automatically.
122
155
 
123
156
  E.g.
124
157
 
125
158
  ```ruby
126
- crystalize [a: :int, b: :int] => :int
127
- def add(a, b)
159
+ crystallize :int
160
+ def add(a: :int, b: :int)
128
161
  puts "Adding #{a} and #{b}"
129
162
  a + b
130
163
  end
131
164
  ```
132
165
 
133
- ### Crystal Compatible
134
-
166
+ ### Crystal-only Syntax
135
167
  Some Crystal syntax is not valid Ruby, for methods of this form, we need to
136
- define our functions using a raw: true option
168
+ define our functions using the `raw: true` option
137
169
 
138
170
  ```ruby
139
- crystalize [a: :int, b: :int] => :int, raw: true
140
- def add(a, b)
171
+ crystallize raw: true
172
+ def add(a: :int, b: :int)
141
173
  <<~CRYSTAL
142
174
  c = 0_u64
143
175
  a + b + c
@@ -145,6 +177,31 @@ def add(a, b)
145
177
  end
146
178
  ```
147
179
 
180
+ ### Upgrading from version 0.2.x
181
+
182
+ #### Change in type signatures
183
+ In version 0.2.x, argument and return types were passed to the `crystallize` method using a different syntax:
184
+
185
+ ```ruby
186
+ # V <= 0.2.x
187
+ crystallize [arg1: :arg1_type , arg2: :arg2_type] => :return_type
188
+ def foo(arg1, arg2)
189
+ ```
190
+
191
+ In crystalruby > 0.3.x, argument types are now passed as keyword arguments, and the return type is passed either as a keyword argument
192
+ or as the first argument to crystallize (either using symbol shorthand, or a Lambda returning a Crystal type).
193
+
194
+ ```ruby
195
+ # V >= 0.3.x
196
+ crystallize :return_type
197
+ def foo(arg1: :arg1_type, arg2: :arg2_type)
198
+
199
+ # OR use the `returns` kwarg
200
+ crystallize
201
+ def foo(arg1: :arg1_type, arg2: :arg2_type, returns: :return_type)
202
+ ```
203
+
204
+
148
205
  ## Getting Started
149
206
 
150
207
  The below is a stand-alone one-file script that allows you to quickly see crystalruby in action.
@@ -160,67 +217,47 @@ end
160
217
 
161
218
  require 'crystalruby'
162
219
 
163
- module Adder
164
- crystalize [a: :int, b: :int] => :int
165
- def add(a, b)
166
- a + b
167
- end
220
+ crystallize :int
221
+ def add(a: :int, b: :int)
222
+ a + b
168
223
  end
169
224
 
170
- puts Adder.add(1, 2)
225
+ puts add(1, 2)
171
226
  ```
172
227
 
173
228
  ## Types
229
+ Most built-in Crystal Types are available. You can also use the `:symbol` short-hand for primitive types.
174
230
 
175
- Currently primitive types are supported.
176
- Composite types are supported using JSON serialization.
177
- C-Structures are a WIP.
178
- To see the list of currently supported primitive type mappings of FFI types to crystal types, you can check: `CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP`
179
- E.g.
231
+ ### Supported Types
232
+ * UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64
233
+ * Time
234
+ * Symbol
235
+ * Nil
236
+ * Bool
237
+ * Container Types (Tuple, Tagged Union, NamedTuple, Array, Hash)
238
+ * Proc
180
239
 
181
- ```ruby
182
- CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP
183
- => {:char=>"Int8",
184
- :uchar=>"UInt8",
185
- :int8=>"Int8",
186
- :uint8=>"UInt8",
187
- :short=>"Int16",
188
- :ushort=>"UInt16",
189
- :int16=>"Int16",
190
- :uint16=>"UInt16",
191
- :int=>"Int32",
192
- :uint=>"UInt32",
193
- :int32=>"Int32",
194
- :uint32=>"UInt32",
195
- :long=>"Int32 | Int64",
196
- :ulong=>"UInt32 | UInt64",
197
- :int64=>"Int64",
198
- :uint64=>"UInt64",
199
- :long_long=>"Int64",
200
- :ulong_long=>"UInt64",
201
- :float=>"Float32",
202
- :double=>"Float64",
203
- :bool=>"Bool",
204
- :void=>"Void",
205
- :string=>"String"}
206
- ```
207
-
208
- ## Composite Types (using JSON serialization)
209
-
210
- The library allows you to pass complex nested structures using JSON as a serialization format.
211
- The type signatures for composite types can use ordinary Crystal Type syntax.
212
- Type conversion is applied automatically.
240
+ Primitive types short-hands
241
+ * :char :uchar :int8 :uint8 :short :ushort :int16 :uint16
242
+ * :int :uint :int32 :uint32 :long :ulong :int64 :uint64
243
+ * :long_long :ulong_long :float :double :bool
244
+ * :void :pointer :string
245
+
246
+ For composite and union types, you can declare these within the function signature, using a syntax similar to Crystal's type syntax.
213
247
 
214
248
  E.g.
215
249
 
216
250
  ```ruby
217
- crystalize [a: json{ Int64 | Float64 | Nil }, b: json{ String | Array(Bool) } ] => :void
218
- def complex_argument_types
251
+ require 'crystalruby'
252
+
253
+ crystallize
254
+ def complex_argument_types(a: Int64 | Float64 | Nil, b: String | Array(Bool))
219
255
  puts "Got #{a} and #{b}"
220
256
  end
221
257
 
222
- crystalize [] => json{ Int32 | String | Hash(String, Array(NamedTuple(hello: Int32)) | Time)}
223
- def complex_return_type
258
+
259
+ crystallize
260
+ def complex_return_type(returns: Int32 | String | Hash(String, Array(NamedTuple(hello: Int32)) | Time))
224
261
  return {
225
262
  "hello" => [
226
263
  {
@@ -230,91 +267,265 @@ def complex_return_type
230
267
  "world" => Time.utc
231
268
  }
232
269
  end
270
+
271
+ complex_argument_types(10, "Hello")
272
+ puts complex_return_type()
233
273
  ```
234
274
 
275
+
235
276
  Type signatures validations are applied to both arguments and return types.
236
277
 
237
278
  ```ruby
238
- [1] pry(main)> Foo.complex_argument_types(nil, "test")
279
+ [1] pry(main)> complex_argument_types(nil, "test")
239
280
  Got and test
240
281
  => nil
241
282
 
242
- [2] pry(main)> Foo.complex_argument_types(88, [true, false, true])
283
+ [2] pry(main)> complex_argument_types(88, [true, false, true])
243
284
  Got 88 and [true, false, true]
244
285
  => nil
245
286
 
246
- [3] pry(main)> Foo.complex_argument_types(88, [true, false, 88])
287
+ [3] pry(main)> complex_argument_types(88, [true, false, 88])
247
288
  ArgumentError: Expected Bool but was Int at line 1, column 15
248
289
  from crystalruby.rb:303:in `block in compile!'
249
290
  ```
250
291
 
251
- ## Named Types
292
+ ### Reference Types
293
+ By default, all types are passed by value, as there is an implicit copy each time a value is passed
294
+ between Crystal and Ruby.
295
+ However, if you name a type you can instantiate it (in either Ruby or Crystal) and pass by reference instead.
296
+ This allows for more efficient passing of large data structures between the two languages.
297
+
298
+ `crystalruby` implements a shared reference counter, so that the same object can be safely used across both languages
299
+ without fear of them being garbage collected prematurely.
252
300
 
253
- You can name your types, for more succinct method signatures.
254
- The type names will be mirrored in the generated Crystal code.
255
301
  E.g.
256
302
 
257
303
  ```ruby
258
304
 
259
- IntArrOrBoolArr = crtype{ Array(Bool) | Array(Int32) }
305
+ IntArrOrBoolArr = CRType{ Array(Bool) | Array(Int32) }
260
306
 
261
- crystalize [a: json{ IntArrOrBoolArr }] => json{ IntArrOrBoolArr }
262
- def method_with_named_types(a)
307
+ crystallize
308
+ def method_with_named_types(a: IntArrOrBoolArr, returns: IntArrOrBoolArr)
263
309
  return a
264
310
  end
311
+
312
+ # In this case the array is converted to a Crystal Array (so a copy is made)
313
+ method_with_named_types([1,2,3])
314
+
315
+ # In this case, no conversion is necessary and the array is passed by reference
316
+ int_array = IntArrOrBoolArr.new([1,2,3]) # Or IntArrOrBoolArr[1,2,3]
317
+ method_with_named_types(int_array)
265
318
  ```
266
319
 
267
- ## Exceptions
320
+ We can demonstrate the significant performance advantage of passing by reference with the following benchmark.
268
321
 
269
- Exceptions thrown in Crystal code can be caught in Ruby.
322
+ ```ruby
323
+ require 'benchmark'
324
+ require 'crystalruby'
325
+
326
+ IntArray = CRType{ Array(Int32) }
327
+
328
+ crystallize
329
+ def array_doubler(a: IntArray)
330
+ a.map! { |x| x * 2 }
331
+ end
332
+
333
+ def array_doubler_rb(a)
334
+ a.map! { |x| x * 2 }
335
+ end
336
+
337
+ big_array = Array.new(1_000_000) { rand(100) }
338
+ big_int_array = IntArray.new(big_array)
339
+
340
+ Benchmark.bm do |x|
341
+ x.report("Crystal Pass by value") { array_doubler(big_array) }
342
+ x.report("Crystal Pass by reference") { array_doubler(big_int_array) }
343
+ x.report("Ruby Pass by reference") { array_doubler_rb(big_array) }
344
+ end
345
+ ```
346
+
347
+ ### Shared Instances
348
+ You can even define instance methods on an instance of a reference type, to make addressable objects that are shared between Ruby and Crystal.
349
+
350
+ ```ruby
351
+ require 'crystalruby'
352
+
353
+ class Person < CRType{ NamedTuple(name: String, age: Int32) }
354
+ def greet_rb
355
+ "Hello from Ruby. My name is #{self.name.value}"
356
+ end
357
+
358
+ crystallize :string
359
+ def greet_cr
360
+ "Hello from Crystal, My name is #{self.name.value}"
361
+ end
362
+ end
363
+
364
+ person = Person.new(name: "Bob", age: 30)
365
+ puts person.greet_rb
366
+ person.name = "Alice"
367
+ puts person.greet_cr
368
+ ```
369
+
370
+ ## Calling Ruby from Crystal
371
+ You can also call Ruby methods from Crystal. To do this, you must annotate the exposed Ruby method with
372
+ `expose_to_crystal` so that crystalruby can perform the appropriate type conversions.
373
+
374
+ ```ruby
375
+ require 'crystalruby'
376
+
377
+ module Adder
378
+ expose_to_crystal :int32
379
+ def add_rb(a: Int32, b: Int32)
380
+ a + b
381
+ end
382
+
383
+ crystallize :int32
384
+ def add_crystal(a: Int32, b: Int32)
385
+ return add_rb(a, b)
386
+ end
387
+ end
388
+
389
+ puts Adder.add_crystal(1, 2)
390
+ ```
391
+
392
+ ### Kemal
393
+ Here's a more realistic example of where you could call Ruby from Crystal.
394
+ We run the Kemal web server in Crystal, but allow certain routes to respond from Ruby, allowing
395
+ us to combine the raw speed of Kemal, with the flexibility of Ruby.
396
+
397
+ ```ruby
398
+ require 'crystalruby'
399
+
400
+ shard :kemal, github: 'kemalcr/kemal'
401
+
402
+ crystallize async: true
403
+ def start_server
404
+ Kemal.run(3000, [""])
405
+ end
406
+
407
+ expose_to_crystal
408
+ def return_ruby_response(returns: String)
409
+ "Hello World! #{Random.rand(0..100)}"
410
+ end
270
411
 
271
- ## Installing shards and writing non-embedded Crystal code
412
+ crystal do
413
+ get "/kemal_rb" do
414
+ return_ruby_response
415
+ end
416
+
417
+ get "/kemal_cr" do
418
+ "Hello World! #{Random.rand(0..100)}"
419
+ end
420
+ end
421
+
422
+ start_server
423
+ ```
272
424
 
273
- You can use any Crystal shards and write ordinary, stand-alone Crystal code.
425
+ We could compare the above to an equivalent pure Ruby implementation using Sinatra.
274
426
 
275
- The default entry point for the crystal shared library generated by the gem is
276
- inside `./crystalruby/{library_name}/src/main.cr`.
277
- `{library_name}` defaults to `crystalruby` if you haven't explicitly specific a different library target.
427
+ ```ruby
428
+ require 'sinatra'
278
429
 
279
- This file is not automatically overridden by the gem, and is safe for you to define and require new files relative to this location to write additional stand-alone Crystal code.
430
+ get '/sinatra_rb' do
431
+ 'Hello world!'
432
+ end
433
+ ```
280
434
 
281
- You can define shard dependencies inside `./crystalruby/{library_name}/src/shard.yml`
282
- Run the below to install new shards
435
+ and benchmark the two.
283
436
 
284
437
  ```bash
285
- bundle exec crystalruby install
438
+
439
+ $ wrk -d 2 http://localhost:4567/kemal_rb
440
+ ... Requests/sec: 23352.00
441
+
442
+ $ wrk -d 2 http://localhost:4567/kemal_cr
443
+ ... Requests/sec: 35730.03
444
+
445
+ $ wrk -d 2 http://localhost:4567/sinatra_rb
446
+ ... Requests/sec: 5300.67
447
+
286
448
  ```
287
449
 
288
- Remember to also require these dependencies after installing them to make them available to `crystalruby` code. E.g. inside `./crystalruby/{libraryname}/src/main.cr`
450
+ Note the hybrid Crystal/Ruby implementation is significantly faster (4x) than the pure Ruby implementation
451
+ and almost 66% as fast as the pure Crystal implementation.
452
+
453
+
454
+ ## Yielding
455
+ crystalruby supports Crystal methods yielding to Ruby, and Ruby blocks yielding to Crystal.
456
+ To support this, you must add a block argument to your method signature, and use the `yield` keyword to call the block.
289
457
 
290
- You can edit the default paths for crystal source and library files from within the `./crystalruby.yaml` config file.
458
+ See notes on how to define a Proc type in Crystal [here](https://crystal-lang.org/reference/1.14/syntax_and_semantics/literals/proc.html#the-proc-type)
459
+
460
+ ```ruby
461
+ require 'crystalruby'
462
+
463
+ crystallize
464
+ def yielder_cr(a: Int32, b: Int32, yield: Proc(Int32, Nil))
465
+ yield a + b
466
+ end
467
+
468
+ expose_to_crystal
469
+ def yielder_rb(a: Int32, b: Int32, yield: Proc(Int32, Nil))
470
+ yield a + b
471
+ end
472
+
473
+ crystallize
474
+ def invoke_yielder_rb(a: Int32, b: Int32)
475
+ yielder_rb(a, b) do |sum|
476
+ puts sum
477
+ end
478
+ end
479
+
480
+ yielder_cr(10, 20){|sum| puts sum } #=> 30
481
+ invoke_yielder_rb(50, 50) #=> 100
482
+ ```
483
+
484
+ ## Exceptions
485
+
486
+ Exceptions thrown in Crystal code can be caught in Ruby.
487
+
488
+ ## Using shards
489
+ You can specify shard dependencies inline in your Ruby source, using the `shard` method.
490
+
491
+ ```ruby
492
+
493
+ shard :redis, github: 'stefanwille/crystal-redis'
494
+
495
+ ```
496
+
497
+ Any options you pass to the `shard` method will be added to the corresponding shard dependency in the autogenerated `shard.yml` file.
498
+ crystalruby will automatically
499
+ * run `shards install` for you
500
+ * require the specified shard
501
+ upon compilation.
502
+
503
+ 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.
291
504
 
292
505
  ## Wrapping Crystal code in Ruby
293
506
 
294
507
  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.
295
- To do this, you simply pass a block to the `crystalize` 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.
508
+ 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.
296
509
 
297
510
  ```ruby
298
- module MyModule
299
- crystalize [a: :int32, b: :int32] => :int32 do |a, b|
300
- # In this example, we perform automated conversion to integers inside Ruby.
301
- # Then add 1 to the result of the Crystal method.
302
- result = super(a.to_i, b.to_i)
303
- result + 1
304
- end
305
- def add(a, b)
306
- a + b
307
- end
511
+ crystallize :int32 do |a, b|
512
+ # In this example, we perform automated conversion to integers inside Ruby.
513
+ # Then add 1 to the result of the Crystal method.
514
+ result = super(a.to_i, b.to_i)
515
+ result + 1
516
+ end
517
+ def convert_to_i_and_add_and_succ(a: :int32, b: :int32)
518
+ a + b
308
519
  end
309
520
 
310
- MyModule.add("1", "2")
521
+ puts convert_to_i_and_add_and_succ("1", "2")
311
522
  ```
312
523
 
313
524
  ## Inline Chunks
314
525
 
315
- `crystalruby` also allows you to write inline Crystal code that does not require binding to Ruby. This can be useful for e.g. performing setup operations or initializations.
526
+ `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.
316
527
 
317
- Follow these steps for a toy example of how we can use crystalized ruby and inline chunks to expose the [crystal-redis](https://github.com/stefanwille/crystal-redis) library to Ruby.
528
+ Follow these steps for a toy example of how we can use crystallized ruby and inline chunks to expose the [crystal-redis](https://github.com/stefanwille/crystal-redis) library to Ruby.
318
529
 
319
530
  1. Start our toy project
320
531
 
@@ -346,6 +557,8 @@ require 'crystalruby'
346
557
 
347
558
  module CrystalRedis
348
559
 
560
+ shard :redis, github: 'stefanwille/crystal-redis'
561
+
349
562
  crystal do
350
563
  CLIENT = Redis.new
351
564
  def self.client
@@ -353,51 +566,55 @@ module CrystalRedis
353
566
  end
354
567
  end
355
568
 
356
- crystalize [key: :string, value: :string] => :void
357
- def set(key, value)
569
+ crystallize
570
+ def set(key: String, value: String)
358
571
  client.set(key, value)
359
572
  end
360
573
 
361
- crystalize [key: :string] => :string
362
- def get(key)
574
+ crystallize :string
575
+ def get(key: String)
363
576
  client.get(key).to_s
364
577
  end
365
578
  end
366
579
  ```
367
580
 
368
- 4. Load the modules (without running them) to generate our Crystal project skeleton.
581
+ 3. Compile and benchmark our new module in Ruby
369
582
 
370
- ```bash
371
- bundle exec ruby crystalredis.rb
372
- ```
583
+ ```ruby
584
+ # Filename: benchmark.rb
585
+ # Let's compare the performance of our CrystalRedis module to the Ruby Redis gem
586
+ require 'crystalruby'
587
+ require 'redis'
588
+ require 'benchmark/ips'
589
+ require 'debug'
373
590
 
374
- 5. Add the missing Redis dependency to our shard.yml
591
+ # For a high IPS single-threaded program, we can set the single_thread_mode to true for faster
592
+ # FFI interop
593
+ CrystalRuby.configure do |config|
594
+ config.single_thread_mode = true
595
+ end
375
596
 
376
- ```yaml
377
- # filename: crystalruby/src/shard.yml
378
- dependencies:
379
- redis:
380
- github: stefanwille/crystal-redis
381
- ```
597
+ module CrystalRedis
382
598
 
383
- ```ruby
384
- # filename: main.cr
385
- require "redis"
386
- require "./generated/index"
387
- ```
599
+ shard :redis, github: 'stefanwille/crystal-redis'
388
600
 
389
- ```bash
390
- bundle exec crystalruby install
391
- ```
601
+ crystal do
602
+ CLIENT = Redis.new
603
+ def self.client
604
+ CLIENT
605
+ end
606
+ end
392
607
 
393
- 6. Compile and benchmark our new module in Ruby
608
+ crystallize
609
+ def set(key: String, value: String)
610
+ client.set(key, value)
611
+ end
394
612
 
395
- ```ruby
396
- # Filename: benchmark.rb
397
- # Let's compare the performance of our CrystalRedis module to the Ruby Redis gem
398
- require_relative "crystalredis"
399
- require 'redis'
400
- require 'benchmark/ips'
613
+ crystallize :string
614
+ def get(key: String)
615
+ client.get(key).to_s
616
+ end
617
+ end
401
618
 
402
619
  Benchmark.ips do |x|
403
620
  rbredis = Redis.new
@@ -412,32 +629,18 @@ Benchmark.ips do |x|
412
629
  rbredis.get("hello")
413
630
  end
414
631
  end
415
- ```
416
-
417
- 7. Run the benchmark
418
632
 
419
- ```bash
420
- $ bundle exec ruby benchmark.rb
421
633
  ```
422
634
 
423
- ### Output
635
+ 4. Run the benchmark
424
636
 
425
637
  ```bash
426
-
427
- #crystalredis wins! (Warm up during first run will be slow for crredis, due to first compilation)
428
-
429
- ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22]
430
- Warming up --------------------------------------
431
- crredis 1.946k i/100ms
432
- rbredis 1.749k i/100ms
433
- Calculating -------------------------------------
434
- crredis 22.319k (± 1.7%) i/s - 112.868k in 5.058448s
435
- rbredis 16.861k (± 9.1%) i/s - 83.952k in 5.024941s
638
+ $ bundle exec ruby benchmark.rb
436
639
  ```
437
640
 
438
641
  ## Release Builds
439
642
 
440
- You can control whether CrystalRuby builds in debug or release mode by setting following config option
643
+ You can control whether crystalruby builds in debug or release mode by setting following config option
441
644
 
442
645
  ```ruby
443
646
  CrystalRuby.configure do |config|
@@ -476,20 +679,20 @@ To safely utilise `crystalruby` in a multithreaded environment, `crystalruby` im
476
679
 
477
680
  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.
478
681
 
479
- To allow you to benefit from Crystal's fiber based concurrency, you can use the `async: true` option on crystalized ruby methods. This allows several Ruby threads to invoke Crystal code simultaneously.
682
+ 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.
480
683
 
481
684
  E.g.
482
685
 
483
686
  ```ruby
484
687
  module Sleeper
485
- crystalize [] => :void
688
+ crystallize
486
689
  def sleep_sync
487
- sleep 2
690
+ sleep 2.seconds
488
691
  end
489
692
 
490
- crystalize [] => :void, async: true
693
+ crystallize async: true
491
694
  def sleep_async
492
- sleep 2
695
+ sleep 2.seconds
493
696
  end
494
697
  end
495
698
  ```
@@ -521,12 +724,12 @@ recompile Crystal code only when it detects changes to the embedded function or
521
724
  ## Multi-library support
522
725
 
523
726
  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.
524
- To indicate which library a piece of embedded Crystal code belongs to, you can use the `lib` option in the `crystalize` and `crystal` methods.
727
+ To indicate which library a piece of embedded Crystal code belongs to, you can use the `lib` option in the `crystallize` and `crystal` methods.
525
728
  If the `lib` option is not provided, the code will be compiled into the default library (simply named `crystalruby`).
526
729
 
527
730
  ```ruby
528
731
  module Foo
529
- crystalize lib: "foo"
732
+ crystallize lib: "foo"
530
733
  def bar
531
734
  puts "Hello from Foo"
532
735
  end
@@ -559,14 +762,7 @@ However, the abstraction it provides should remain simple, transparent, and easy
559
762
 
560
763
  It should support escape hatches to allow it to coexist with code that performs a more direct [FFI](https://github.com/ffi/ffi) integration to implement advanced functionality not supported by `crystalruby`.
561
764
 
562
- The library is currently in its infancy. Planned additions are:
563
-
564
- - Simple mixin/concern that utilises `FFI::Struct` for bi-directional passing of Ruby objects and Crystal objects (by value).
565
- - Install command to generate a sample build script, and supports build command (which simply verifies then invokes this script)
566
- - Call Ruby from Crystal using FFI callbacks (implement `.expose_to_crystal`)
567
- - Support long-lived synchronized objects (through use of synchronized memory arena to prevent GC).
568
- - Support for passing `crystalruby` types by reference (need to contend with GC).
569
- - Explore mechanisms to safely expose true parallelism using [FFI over Ractors](https://github.com/ffi/ffi/wiki/Ractors)
765
+ The library is currently in its infancy.
570
766
 
571
767
  ## Installation
572
768
 
@@ -592,7 +788,7 @@ $ gem install crystalruby
592
788
  You can run `crystalruby init` to generate a configuration file with sane defaults.
593
789
 
594
790
  ```bash
595
- crystalruby init
791
+ $ crystalruby init
596
792
  ```
597
793
 
598
794
  ```yaml
@@ -610,6 +806,7 @@ Alternatively, these can be set programmatically, e.g:
610
806
  CrystalRuby.configure do |config|
611
807
  config.crystal_src_dir = "./crystalruby"
612
808
  config.crystal_codegen_dir = "generated"
809
+ config.crystal_missing_ignore = false
613
810
  config.debug = true
614
811
  config.verbose = false
615
812
  config.colorize_log_output = false