crystalruby 0.2.3 → 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 +389 -193
- 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 +32 -3
- 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
|
```
|
@@ -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
|