crystalruby 0.2.2 → 0.3.0

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