crystalruby 0.2.2 → 0.3.0
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/README.md +391 -195
- data/Rakefile +4 -3
- data/crystalruby.gemspec +2 -2
- data/exe/crystalruby +1 -0
- data/lib/crystalruby/adapter.rb +131 -65
- data/lib/crystalruby/arc_mutex.rb +47 -0
- data/lib/crystalruby/compilation.rb +33 -4
- data/lib/crystalruby/config.rb +41 -37
- data/lib/crystalruby/function.rb +211 -68
- data/lib/crystalruby/library.rb +153 -48
- data/lib/crystalruby/reactor.rb +40 -23
- data/lib/crystalruby/source_reader.rb +86 -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 +109 -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 +239 -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 +41 -19
- 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
|
+
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
|
-
|
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
|
+
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
|
-
|
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 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
|
-
|
96
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
167
|
+
define our functions using the `raw: true` option
|
137
168
|
|
138
169
|
```ruby
|
139
|
-
crystalize
|
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
|
-
|
164
|
-
|
165
|
-
|
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
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
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.
|
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
|
-
|
218
|
-
|
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
|
-
|
223
|
-
|
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)>
|
278
|
+
[1] pry(main)> complex_argument_types(nil, "test")
|
239
279
|
Got and test
|
240
280
|
=> nil
|
241
281
|
|
242
|
-
[2] pry(main)>
|
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)>
|
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
|
-
|
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 =
|
304
|
+
IntArrOrBoolArr = CRType{ Array(Bool) | Array(Int32) }
|
260
305
|
|
261
|
-
crystalize
|
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
|
-
|
319
|
+
We can demonstrate the significant performance advantage of passing by reference with the following benchmark.
|
268
320
|
|
269
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
276
|
-
|
277
|
-
`{library_name}` defaults to `crystalruby` if you haven't explicitly specific a different library target.
|
373
|
+
```ruby
|
374
|
+
require 'crystalruby'
|
278
375
|
|
279
|
-
|
376
|
+
module Adder
|
377
|
+
expose_to_crystal :int32
|
378
|
+
def add_rb(a: Int32, b: Int32)
|
379
|
+
a + b
|
380
|
+
end
|
280
381
|
|
281
|
-
|
282
|
-
|
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
|
-
|
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
|
-
|
483
|
+
## Exceptions
|
289
484
|
|
290
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
580
|
+
3. Compile and benchmark our new module in Ruby
|
369
581
|
|
370
|
-
```
|
371
|
-
|
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
|
-
|
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
|
-
|
377
|
-
# filename: crystalruby/src/shard.yml
|
378
|
-
dependencies:
|
379
|
-
redis:
|
380
|
-
github: stefanwille/crystal-redis
|
381
|
-
```
|
596
|
+
module CrystalRedis
|
382
597
|
|
383
|
-
|
384
|
-
# filename: main.cr
|
385
|
-
require "redis"
|
386
|
-
require "./generated/index"
|
387
|
-
```
|
598
|
+
shard :redis, github: 'stefanwille/crystal-redis'
|
388
599
|
|
389
|
-
|
390
|
-
|
391
|
-
|
600
|
+
crystal do
|
601
|
+
CLIENT = Redis.new
|
602
|
+
def self.client
|
603
|
+
CLIENT
|
604
|
+
end
|
605
|
+
end
|
392
606
|
|
393
|
-
|
607
|
+
crystalize
|
608
|
+
def set(key: String, value: String)
|
609
|
+
client.set(key, value)
|
610
|
+
end
|
394
611
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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
|
-
|
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
|
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
|
687
|
+
crystalize
|
486
688
|
def sleep_sync
|
487
|
-
sleep 2
|
689
|
+
sleep 2.seconds
|
488
690
|
end
|
489
691
|
|
490
|
-
crystalize
|
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
|
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
|
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.
|
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
|