crystalruby 0.2.3 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -0
- data/Dockerfile +23 -2
- data/README.md +395 -198
- data/Rakefile +4 -3
- data/crystalruby.gemspec +2 -2
- data/examples/adder/adder.rb +1 -1
- data/exe/crystalruby +1 -0
- data/lib/crystalruby/adapter.rb +143 -73
- data/lib/crystalruby/arc_mutex.rb +47 -0
- data/lib/crystalruby/compilation.rb +32 -3
- data/lib/crystalruby/config.rb +41 -37
- data/lib/crystalruby/function.rb +216 -73
- data/lib/crystalruby/library.rb +157 -51
- data/lib/crystalruby/reactor.rb +63 -44
- data/lib/crystalruby/source_reader.rb +92 -0
- data/lib/crystalruby/template.rb +16 -5
- data/lib/crystalruby/templates/function.cr +11 -10
- data/lib/crystalruby/templates/index.cr +53 -66
- data/lib/crystalruby/templates/inline_chunk.cr +1 -1
- data/lib/crystalruby/templates/ruby_interface.cr +34 -0
- data/lib/crystalruby/templates/top_level_function.cr +62 -0
- data/lib/crystalruby/templates/top_level_ruby_interface.cr +33 -0
- data/lib/crystalruby/typebuilder.rb +11 -55
- data/lib/crystalruby/typemaps.rb +92 -67
- data/lib/crystalruby/types/concerns/allocator.rb +80 -0
- data/lib/crystalruby/types/fixed_width/named_tuple.cr +80 -0
- data/lib/crystalruby/types/fixed_width/named_tuple.rb +86 -0
- data/lib/crystalruby/types/fixed_width/proc.cr +45 -0
- data/lib/crystalruby/types/fixed_width/proc.rb +79 -0
- data/lib/crystalruby/types/fixed_width/tagged_union.cr +53 -0
- data/lib/crystalruby/types/fixed_width/tagged_union.rb +113 -0
- data/lib/crystalruby/types/fixed_width/tuple.cr +82 -0
- data/lib/crystalruby/types/fixed_width/tuple.rb +92 -0
- data/lib/crystalruby/types/fixed_width.cr +138 -0
- data/lib/crystalruby/types/fixed_width.rb +205 -0
- data/lib/crystalruby/types/primitive.cr +21 -0
- data/lib/crystalruby/types/primitive.rb +117 -0
- data/lib/crystalruby/types/primitive_types/bool.cr +34 -0
- data/lib/crystalruby/types/primitive_types/bool.rb +11 -0
- data/lib/crystalruby/types/primitive_types/nil.cr +35 -0
- data/lib/crystalruby/types/primitive_types/nil.rb +16 -0
- data/lib/crystalruby/types/primitive_types/numbers.cr +37 -0
- data/lib/crystalruby/types/primitive_types/numbers.rb +28 -0
- data/lib/crystalruby/types/primitive_types/symbol.cr +55 -0
- data/lib/crystalruby/types/primitive_types/symbol.rb +35 -0
- data/lib/crystalruby/types/primitive_types/time.cr +35 -0
- data/lib/crystalruby/types/primitive_types/time.rb +25 -0
- data/lib/crystalruby/types/type.cr +64 -0
- data/lib/crystalruby/types/type.rb +249 -30
- data/lib/crystalruby/types/variable_width/array.cr +74 -0
- data/lib/crystalruby/types/variable_width/array.rb +88 -0
- data/lib/crystalruby/types/variable_width/hash.cr +146 -0
- data/lib/crystalruby/types/variable_width/hash.rb +117 -0
- data/lib/crystalruby/types/variable_width/string.cr +36 -0
- data/lib/crystalruby/types/variable_width/string.rb +18 -0
- data/lib/crystalruby/types/variable_width.cr +23 -0
- data/lib/crystalruby/types/variable_width.rb +46 -0
- data/lib/crystalruby/types.rb +32 -13
- data/lib/crystalruby/version.rb +2 -2
- data/lib/crystalruby.rb +13 -6
- metadata +42 -22
- data/lib/crystalruby/types/array.rb +0 -15
- data/lib/crystalruby/types/bool.rb +0 -3
- data/lib/crystalruby/types/hash.rb +0 -17
- data/lib/crystalruby/types/named_tuple.rb +0 -28
- data/lib/crystalruby/types/nil.rb +0 -3
- data/lib/crystalruby/types/numbers.rb +0 -5
- data/lib/crystalruby/types/string.rb +0 -3
- data/lib/crystalruby/types/symbol.rb +0 -3
- data/lib/crystalruby/types/time.rb +0 -8
- data/lib/crystalruby/types/tuple.rb +0 -17
- data/lib/crystalruby/types/type_serializer/json.rb +0 -41
- data/lib/crystalruby/types/type_serializer.rb +0 -37
- data/lib/crystalruby/types/typedef.rb +0 -57
- data/lib/crystalruby/types/union_type.rb +0 -43
- 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.
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
(2..
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
78
|
-
puts
|
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
|
-
|
84
|
-
0.
|
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
|
-
|
96
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
168
|
+
define our functions using the `raw: true` option
|
137
169
|
|
138
170
|
```ruby
|
139
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
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
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
:
|
185
|
-
:
|
186
|
-
|
187
|
-
|
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
|
-
|
218
|
-
|
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
|
-
|
223
|
-
|
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)>
|
279
|
+
[1] pry(main)> complex_argument_types(nil, "test")
|
239
280
|
Got and test
|
240
281
|
=> nil
|
241
282
|
|
242
|
-
[2] pry(main)>
|
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)>
|
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
|
-
|
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 =
|
305
|
+
IntArrOrBoolArr = CRType{ Array(Bool) | Array(Int32) }
|
260
306
|
|
261
|
-
|
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
|
-
|
320
|
+
We can demonstrate the significant performance advantage of passing by reference with the following benchmark.
|
268
321
|
|
269
|
-
|
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
|
-
|
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
|
-
|
425
|
+
We could compare the above to an equivalent pure Ruby implementation using Sinatra.
|
274
426
|
|
275
|
-
|
276
|
-
|
277
|
-
`{library_name}` defaults to `crystalruby` if you haven't explicitly specific a different library target.
|
427
|
+
```ruby
|
428
|
+
require 'sinatra'
|
278
429
|
|
279
|
-
|
430
|
+
get '/sinatra_rb' do
|
431
|
+
'Hello world!'
|
432
|
+
end
|
433
|
+
```
|
280
434
|
|
281
|
-
|
282
|
-
Run the below to install new shards
|
435
|
+
and benchmark the two.
|
283
436
|
|
284
437
|
```bash
|
285
|
-
|
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
|
-
|
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
|
-
|
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 `
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
581
|
+
3. Compile and benchmark our new module in Ruby
|
369
582
|
|
370
|
-
```
|
371
|
-
|
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
|
-
|
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
|
-
|
377
|
-
# filename: crystalruby/src/shard.yml
|
378
|
-
dependencies:
|
379
|
-
redis:
|
380
|
-
github: stefanwille/crystal-redis
|
381
|
-
```
|
597
|
+
module CrystalRedis
|
382
598
|
|
383
|
-
|
384
|
-
# filename: main.cr
|
385
|
-
require "redis"
|
386
|
-
require "./generated/index"
|
387
|
-
```
|
599
|
+
shard :redis, github: 'stefanwille/crystal-redis'
|
388
600
|
|
389
|
-
|
390
|
-
|
391
|
-
|
601
|
+
crystal do
|
602
|
+
CLIENT = Redis.new
|
603
|
+
def self.client
|
604
|
+
CLIENT
|
605
|
+
end
|
606
|
+
end
|
392
607
|
|
393
|
-
|
608
|
+
crystallize
|
609
|
+
def set(key: String, value: String)
|
610
|
+
client.set(key, value)
|
611
|
+
end
|
394
612
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
688
|
+
crystallize
|
486
689
|
def sleep_sync
|
487
|
-
sleep 2
|
690
|
+
sleep 2.seconds
|
488
691
|
end
|
489
692
|
|
490
|
-
|
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 `
|
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
|
-
|
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.
|
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
|