crystalruby 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/README.md +389 -193
  4. data/Rakefile +4 -3
  5. data/crystalruby.gemspec +2 -2
  6. data/exe/crystalruby +1 -0
  7. data/lib/crystalruby/adapter.rb +131 -65
  8. data/lib/crystalruby/arc_mutex.rb +47 -0
  9. data/lib/crystalruby/compilation.rb +32 -3
  10. data/lib/crystalruby/config.rb +41 -37
  11. data/lib/crystalruby/function.rb +211 -68
  12. data/lib/crystalruby/library.rb +153 -48
  13. data/lib/crystalruby/reactor.rb +40 -23
  14. data/lib/crystalruby/source_reader.rb +86 -0
  15. data/lib/crystalruby/template.rb +16 -5
  16. data/lib/crystalruby/templates/function.cr +11 -10
  17. data/lib/crystalruby/templates/index.cr +53 -66
  18. data/lib/crystalruby/templates/inline_chunk.cr +1 -1
  19. data/lib/crystalruby/templates/ruby_interface.cr +34 -0
  20. data/lib/crystalruby/templates/top_level_function.cr +62 -0
  21. data/lib/crystalruby/templates/top_level_ruby_interface.cr +33 -0
  22. data/lib/crystalruby/typebuilder.rb +11 -55
  23. data/lib/crystalruby/typemaps.rb +92 -67
  24. data/lib/crystalruby/types/concerns/allocator.rb +80 -0
  25. data/lib/crystalruby/types/fixed_width/named_tuple.cr +80 -0
  26. data/lib/crystalruby/types/fixed_width/named_tuple.rb +86 -0
  27. data/lib/crystalruby/types/fixed_width/proc.cr +45 -0
  28. data/lib/crystalruby/types/fixed_width/proc.rb +79 -0
  29. data/lib/crystalruby/types/fixed_width/tagged_union.cr +53 -0
  30. data/lib/crystalruby/types/fixed_width/tagged_union.rb +109 -0
  31. data/lib/crystalruby/types/fixed_width/tuple.cr +82 -0
  32. data/lib/crystalruby/types/fixed_width/tuple.rb +92 -0
  33. data/lib/crystalruby/types/fixed_width.cr +138 -0
  34. data/lib/crystalruby/types/fixed_width.rb +205 -0
  35. data/lib/crystalruby/types/primitive.cr +21 -0
  36. data/lib/crystalruby/types/primitive.rb +117 -0
  37. data/lib/crystalruby/types/primitive_types/bool.cr +34 -0
  38. data/lib/crystalruby/types/primitive_types/bool.rb +11 -0
  39. data/lib/crystalruby/types/primitive_types/nil.cr +35 -0
  40. data/lib/crystalruby/types/primitive_types/nil.rb +16 -0
  41. data/lib/crystalruby/types/primitive_types/numbers.cr +37 -0
  42. data/lib/crystalruby/types/primitive_types/numbers.rb +28 -0
  43. data/lib/crystalruby/types/primitive_types/symbol.cr +55 -0
  44. data/lib/crystalruby/types/primitive_types/symbol.rb +35 -0
  45. data/lib/crystalruby/types/primitive_types/time.cr +35 -0
  46. data/lib/crystalruby/types/primitive_types/time.rb +25 -0
  47. data/lib/crystalruby/types/type.cr +64 -0
  48. data/lib/crystalruby/types/type.rb +239 -30
  49. data/lib/crystalruby/types/variable_width/array.cr +74 -0
  50. data/lib/crystalruby/types/variable_width/array.rb +88 -0
  51. data/lib/crystalruby/types/variable_width/hash.cr +146 -0
  52. data/lib/crystalruby/types/variable_width/hash.rb +117 -0
  53. data/lib/crystalruby/types/variable_width/string.cr +36 -0
  54. data/lib/crystalruby/types/variable_width/string.rb +18 -0
  55. data/lib/crystalruby/types/variable_width.cr +23 -0
  56. data/lib/crystalruby/types/variable_width.rb +46 -0
  57. data/lib/crystalruby/types.rb +32 -13
  58. data/lib/crystalruby/version.rb +2 -2
  59. data/lib/crystalruby.rb +13 -6
  60. metadata +41 -19
  61. data/lib/crystalruby/types/array.rb +0 -15
  62. data/lib/crystalruby/types/bool.rb +0 -3
  63. data/lib/crystalruby/types/hash.rb +0 -17
  64. data/lib/crystalruby/types/named_tuple.rb +0 -28
  65. data/lib/crystalruby/types/nil.rb +0 -3
  66. data/lib/crystalruby/types/numbers.rb +0 -5
  67. data/lib/crystalruby/types/string.rb +0 -3
  68. data/lib/crystalruby/types/symbol.rb +0 -3
  69. data/lib/crystalruby/types/time.rb +0 -8
  70. data/lib/crystalruby/types/tuple.rb +0 -17
  71. data/lib/crystalruby/types/type_serializer/json.rb +0 -41
  72. data/lib/crystalruby/types/type_serializer.rb +0 -37
  73. data/lib/crystalruby/types/typedef.rb +0 -57
  74. data/lib/crystalruby/types/union_type.rb +0 -43
  75. data/lib/module.rb +0 -3
@@ -1,16 +1,22 @@
1
+ require "forwardable"
2
+
1
3
  module CrystalRuby
2
4
  # This class represents a single Crystalized function.
3
5
  # Each such function belongs a shared lib (See: CrystalRuby::Library)
4
6
  # and is attached to a single owner (a class or a module).
5
7
  class Function
8
+ extend Forwardable
6
9
  include Typemaps
7
10
  include Config
8
11
 
9
- attr_accessor :owner, :method_name, :args, :returns, :function_body, :lib, :async, :block, :attached
12
+ attr_accessor :original_method, :owner, :args, :returns, :function_body, :arity,
13
+ :lib, :async, :block, :attached, :ruby, :instance_method, :class_method
14
+
15
+ def_delegators :@original_method, :name
10
16
 
11
- def initialize(method:, args:, returns:, function_body:, lib:, async: false, &block)
17
+ def initialize(method:, args:, returns:, lib:, function_body: nil, async: false, ruby: false, &block)
18
+ self.original_method = method
12
19
  self.owner = method.owner
13
- self.method_name = method.name
14
20
  self.args = args
15
21
  self.returns = returns
16
22
  self.function_body = function_body
@@ -18,6 +24,16 @@ module CrystalRuby
18
24
  self.async = async
19
25
  self.block = block
20
26
  self.attached = false
27
+ self.class_method = owner.singleton_class? && owner.attached_object.class == Class
28
+ self.instance_method = original_method.is_a?(UnboundMethod) && original_method.owner.ancestors.include?(CrystalRuby::Types::Type)
29
+ self.ruby = ruby
30
+ self.arity = args.keys.-([:__yield_to]).size
31
+ end
32
+
33
+ def crystal_supertype
34
+ return nil unless original_method.owner.ancestors.include?(CrystalRuby::Types::Type)
35
+
36
+ original_method.owner.crystal_supertype
21
37
  end
22
38
 
23
39
  # This is where we write/overwrite the class and instance methods
@@ -28,60 +44,69 @@ module CrystalRuby
28
44
  # will result in a new digest and the FFI function will be recompiled and reattached.
29
45
  def define_crystalized_methods!(lib)
30
46
  func = self
31
- [owner, owner.singleton_class].each do |receiver|
32
- receiver.undef_method(method_name) if receiver.method_defined?(method_name)
33
- receiver.define_method(method_name) do |*args|
34
- unless lib.compiled?
35
- lib.build!
36
- return send(func.method_name, *args)
37
- end
47
+ receivers = instance_method ? [owner] : [owner, owner.singleton_class]
48
+ receivers.each do |receiver|
49
+ receiver.undef_method(name) if receiver.method_defined?(name)
50
+ receiver.define_method(name) do |*args, &blk|
38
51
  unless func.attached?
39
- should_reenter = func.attach_ffi_lib_functions!
40
- return send(func.method_name, *args) if should_reenter
52
+ should_reenter = func.unwrapped?
53
+ lib.build! unless lib.compiled?
54
+ lib.attach! unless func.attached?
55
+ return send(func.name, *args, &blk) if should_reenter
41
56
  end
42
57
  # All crystalruby functions are executed on the reactor to ensure Crystal/Ruby interop code is executed
43
58
  # from a single same thread. (Needed to make GC and Fiber scheduler happy)
44
59
  # Type mapping (if required) is applied on arguments and on return values.
45
- func.map_retval(
46
- Reactor.schedule_work!(
47
- func.owner,
48
- func.ffi_name,
49
- *func.map_args(args),
50
- func.ffi_ret_type,
51
- async: func.async,
52
- lib: lib
53
- )
60
+ if args.length != func.arity
61
+ raise ArgumentError,
62
+ "wrong number of arguments (given #{args.length}, expected #{func.arity})"
63
+ end
64
+
65
+ raise ArgumentError, "block given but function does not accept block" if blk && !func.takes_block?
66
+ raise ArgumentError, "no block given but function expects block" if !blk && func.takes_block?
67
+
68
+ args << blk if blk
69
+ func.map_args!(args)
70
+ args.unshift(memory) if func.instance_method
71
+
72
+ ret_val = Reactor.schedule_work!(
73
+ func.owner,
74
+ func.ffi_name,
75
+ *args,
76
+ func.ffi_ret_type,
77
+ async: func.async,
78
+ lib: lib
54
79
  )
80
+
81
+ func.map_retval(ret_val)
55
82
  end
56
83
  end
57
84
  end
58
85
 
59
- # This is where we attach the top-level FFI functions of the shared object
60
- # to our library (yield and init) needed for successful operation of the reactor.
61
- # We also initialize the shared object (needed to start the GC) and
62
- # start the reactor, unless we are in single-thread mode.
63
- def attach_ffi_lib_functions!
64
- should_reenter = unwrapped?
65
- lib_file = lib.lib_file
66
- lib.methods.each_value(&:attach_ffi_func!)
67
- lib.singleton_class.class_eval do
68
- extend FFI::Library
69
- ffi_lib lib_file
70
- %i[yield init].each do |method_name|
71
- singleton_class.undef_method(method_name) if singleton_class.method_defined?(method_name)
72
- undef_method(method_name) if method_defined?(method_name)
73
- end
74
- attach_function :init, %i[string pointer], :void
75
- attach_function :yield, %i[], :int
76
- end
86
+ def register_callback!
87
+ return unless ruby
77
88
 
78
- if CrystalRuby.config.single_thread_mode
79
- Reactor.init_single_thread_mode!
80
- else
81
- Reactor.start!
89
+ @callback_func = FFI::Function.new(ffi_ret_type, ffi_types) do |*args|
90
+ receiver = instance_method ? owner.new(args.shift) : owner
91
+ ret_val = if takes_block?
92
+ block_arg = arg_type_map[:__yield_to][:crystalruby_type].new(args.pop)
93
+ receiver.send(name, *unmap_args(args)) do |*args|
94
+ args = args.map.with_index do |arg, i|
95
+ arg = block_arg.inner_types[i].new(arg) unless arg.is_a?(block_arg.inner_types[i])
96
+ arg.memory
97
+ end
98
+ return_val = block_arg.invoke(*args)
99
+ unless return_val.is_a?(block_arg.inner_types[-1])
100
+ return_val = block_arg.inner_types[-1].new(return_val)
101
+ end
102
+ block_arg.inner_types[-1].anonymous? ? return_val.value : return_val
103
+ end
104
+ else
105
+ receiver.send(name, *unmap_args(args))
106
+ end
107
+ unmap_retval(ret_val)
82
108
  end
83
- Reactor.schedule_work!(lib, :init, lib.name, Reactor::ERROR_CALLBACK, :void, blocking: true, async: false)
84
- should_reenter
109
+ Reactor.schedule_work!(lib, :"register_#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback", @callback_func, :void, blocking: true, async: false)
85
110
  end
86
111
 
87
112
  # Attaches the crystalized FFI functions to their related Ruby modules and classes.
@@ -111,7 +136,7 @@ module CrystalRuby
111
136
 
112
137
  owner.attach_function ffi_name, argtypes, rettype, blocking: true
113
138
  around_wrapper_block = block
114
- method_name = self.method_name
139
+ method_name = name
115
140
  @attached = true
116
141
  return unless around_wrapper_block
117
142
 
@@ -124,10 +149,6 @@ module CrystalRuby
124
149
  end
125
150
  @around_wrapper.undef_method(method_name) if @around_wrapper.method_defined?(method_name)
126
151
  @around_wrapper.define_method(method_name, &around_wrapper_block)
127
- rescue StandardError => e
128
- CrystalRuby.log_error("Error attaching #{method_name} as #{ffi_name} to #{owner.name}")
129
- CrystalRuby.log_error(e.message)
130
- CrystalRuby.log_error(e.backtrace.join("\n"))
131
152
  end
132
153
 
133
154
  def unwrapped?
@@ -142,12 +163,22 @@ module CrystalRuby
142
163
  @attached = false
143
164
  end
144
165
 
166
+ def owner
167
+ class_method ? @owner.attached_object : @owner
168
+ end
169
+
170
+ def owner_name
171
+ owner.name
172
+ end
173
+
145
174
  def ffi_name
146
175
  lib_fn_name + (async && !config.single_thread_mode ? "_async" : "")
147
176
  end
148
177
 
149
178
  def lib_fn_name
150
- @lib_fn_name ||= "#{owner.name.downcase.gsub("::", "_")}_#{method_name}_#{Digest::MD5.hexdigest(function_body)}"
179
+ @lib_fn_name ||= "#{owner_name.downcase.gsub("::",
180
+ "_")}_#{name.to_s.gsub("?", "query").gsub("!", "bang").gsub("=",
181
+ "eq")}_#{Digest::MD5.hexdigest(function_body.to_s)}"
151
182
  end
152
183
 
153
184
  def arg_type_map
@@ -155,15 +186,29 @@ module CrystalRuby
155
186
  end
156
187
 
157
188
  def lib_fn_args
158
- @lib_fn_args ||= arg_type_map.map { |k, arg_type|
159
- "_#{k} : #{arg_type[:lib_type]}"
160
- }.join(",") + (arg_type_map.empty? ? "" : ", ")
189
+ @lib_fn_args ||= begin
190
+ lib_fn_args = arg_type_map.map do |k, arg_type|
191
+ "_#{k} : #{arg_type[:lib_type]}"
192
+ end
193
+ lib_fn_args.unshift("_self : Pointer(::UInt8)") if instance_method
194
+ lib_fn_args.join(",") + (lib_fn_args.empty? ? "" : ", ")
195
+ end
161
196
  end
162
197
 
163
- def lib_fn_arg_names
164
- @lib_fn_arg_names ||= arg_type_map.map { |k, _arg_type|
165
- "_#{k}"
166
- }.join(",") + (arg_type_map.empty? ? "" : ", ")
198
+ def lib_fn_arg_names(skip_blocks = false)
199
+ @lib_fn_arg_names ||= begin
200
+ names = arg_type_map.keys.reject { |k, _v| skip_blocks && is_block_arg?(k) }.map { |k| "_#{k}" }
201
+ names.unshift("self.memory") if instance_method
202
+ names.join(",") + (names.empty? ? "" : ", ")
203
+ end
204
+ end
205
+
206
+ def lib_fn_types
207
+ @lib_fn_types ||= begin
208
+ lib_fn_types = arg_type_map.map { |_k, v| v[:lib_type] }
209
+ lib_fn_types.unshift("Pointer(::UInt8)") if instance_method
210
+ lib_fn_types.join(",") + (lib_fn_types.empty? ? "" : ", ")
211
+ end
167
212
  end
168
213
 
169
214
  def return_type_map
@@ -171,31 +216,69 @@ module CrystalRuby
171
216
  end
172
217
 
173
218
  def ffi_types
174
- @ffi_types ||= arg_type_map.map { |_k, arg_type| arg_type[:ffi_type] }
219
+ @ffi_types ||= begin
220
+ ffi_types = arg_type_map.map { |_k, arg_type| arg_type[:ffi_type] }
221
+ ffi_types.unshift(:pointer) if instance_method
222
+ ffi_types
223
+ end
175
224
  end
176
225
 
177
226
  def arg_maps
178
227
  @arg_maps ||= arg_type_map.map { |_k, arg_type| arg_type[:arg_mapper] }
179
228
  end
180
229
 
230
+ def arg_unmaps
231
+ @arg_unmaps ||= arg_type_map.reject { |k, _v| is_block_arg?(k) }.map { |_k, arg_type| arg_type[:retval_mapper] }
232
+ end
233
+
181
234
  def ffi_ret_type
182
235
  @ffi_ret_type ||= return_type_map[:ffi_ret_type]
183
236
  end
184
237
 
238
+ def custom_types
239
+ @custom_types ||= begin
240
+ types = [*arg_type_map.values, return_type_map].map { |t| t[:crystalruby_type] }
241
+ types.unshift(owner) if instance_method
242
+ types
243
+ end
244
+ end
245
+
185
246
  def register_custom_types!(lib)
186
- [*arg_type_map.values, return_type_map].map { |t| t[:crystal_ruby_type] }.each do |crystalruby_type|
187
- if crystalruby_type.is_a?(Types::TypeSerializer) && !crystalruby_type.anonymous?
188
- lib.register_type!(crystalruby_type)
247
+ custom_types.each do |crystalruby_type|
248
+ next unless crystalruby_type.is_a?(Class) && crystalruby_type < Types::Type
249
+
250
+ [*crystalruby_type.nested_types].uniq.each do |type|
251
+ lib.register_type!(type)
189
252
  end
190
253
  end
191
254
  end
192
255
 
193
- def map_args(args)
256
+ def map_args!(args)
194
257
  return args unless arg_maps.any?
195
258
 
259
+ refs = nil
260
+
196
261
  arg_maps.each_with_index do |argmap, index|
197
262
  next unless argmap
198
263
 
264
+ mapped = argmap[args[index]]
265
+ case mapped
266
+ when CrystalRuby::Types::Type then
267
+ args[index] = mapped.memory
268
+ (refs ||= []) << mapped
269
+ else
270
+ args[index] = mapped
271
+ end
272
+ end
273
+ refs
274
+ end
275
+
276
+ def unmap_args(args)
277
+ return args unless args.any?
278
+
279
+ arg_unmaps.each_with_index do |argmap, index|
280
+ next unless argmap
281
+
199
282
  args[index] = argmap[args[index]]
200
283
  end
201
284
  args
@@ -207,16 +290,76 @@ module CrystalRuby
207
290
  return_type_map[:retval_mapper][retval]
208
291
  end
209
292
 
293
+ def unmap_retval(retval)
294
+ return retval unless return_type_map[:arg_mapper]
295
+
296
+ retval = return_type_map[:arg_mapper][retval]
297
+ retval = retval.memory if retval.kind_of?(CrystalRuby::Types::Type)
298
+ retval
299
+ end
300
+
301
+ def takes_block?
302
+ is_block_arg?(:__yield_to)
303
+ end
304
+
305
+ def is_block_arg?(arg_name)
306
+ arg_name == :__yield_to && arg_type_map[arg_name] && arg_type_map[arg_name][:crystalruby_type].ancestors.select do |a|
307
+ a < Types::Type
308
+ end.map(&:typename).any?(:Proc)
309
+ end
310
+
311
+ def ruby_interface
312
+ template = owner == Object ? Template::TopLevelRubyInterface : Template::RubyInterface
313
+ @ruby_interface ||= template.render(
314
+ {
315
+ module_or_class: instance_method || class_method ? "class" : "module",
316
+ receiver: instance_method ? "#{owner_name}.new(_self)" : owner_name,
317
+ fn_scope: instance_method ? "" : "self.",
318
+ superclass: instance_method || class_method ? "< #{crystal_supertype}" : nil,
319
+ module_name: owner_name,
320
+ lib_fn_name: lib_fn_name,
321
+ fn_name: name,
322
+ callback_name: "#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback",
323
+ fn_body: function_body,
324
+ block_converter: takes_block? ? arg_type_map[:__yield_to][:crystalruby_type].block_converter : "",
325
+ callback_call: returns == :void ? "callback.call(thread_id)" : "callback.call(thread_id, converted)",
326
+ callback_type: return_type_map[:ffi_type] == :void ? "UInt32 -> Void" : " UInt32, #{return_type_map[:lib_type]} -> Void",
327
+ fn_args: arg_type_map
328
+ .map { |k, arg_type| "#{is_block_arg?(k) ? "&" : ""}#{k} : #{arg_type[:crystal_type]}" }.join(","),
329
+ fn_ret_type: return_type_map[:crystal_type],
330
+ lib_fn_args: lib_fn_args,
331
+ lib_fn_types: lib_fn_types,
332
+ lib_fn_arg_names: lib_fn_arg_names,
333
+ lib_fn_ret_type: return_type_map[:lib_type],
334
+ convert_lib_args: arg_type_map.map do |k, arg_type|
335
+ "_#{k} = #{arg_type[:convert_crystal_to_lib_type]["#{k}"]}"
336
+ end.join("\n "),
337
+ arg_names: args.keys.reject(&method(:is_block_arg?)).join(", "),
338
+ convert_return_type: return_type_map[:convert_lib_to_crystal_type]["return_value"],
339
+ error_value: return_type_map[:error_value]
340
+ }
341
+ )
342
+ end
343
+
210
344
  def chunk
211
- @chunk ||= Template::Function.render(
345
+ template = owner == Object ? Template::TopLevelFunction : Template::Function
346
+ @chunk ||= template.render(
212
347
  {
213
- module_name: owner.name,
348
+ module_or_class: instance_method || class_method ? "class" : "module",
349
+ receiver: instance_method ? "#{owner_name}.new(_self)" : owner_name,
350
+ fn_scope: instance_method ? "" : "self.",
351
+ superclass: instance_method || class_method ? "< #{crystal_supertype}" : nil,
352
+ module_name: owner_name,
214
353
  lib_fn_name: lib_fn_name,
215
- fn_name: method_name,
354
+ fn_name: name,
355
+ callback_name: "#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback",
216
356
  fn_body: function_body,
357
+ block_converter: takes_block? ? arg_type_map[:__yield_to][:crystalruby_type].block_converter : "",
217
358
  callback_call: returns == :void ? "callback.call(thread_id)" : "callback.call(thread_id, converted)",
218
359
  callback_type: return_type_map[:ffi_type] == :void ? "UInt32 -> Void" : " UInt32, #{return_type_map[:lib_type]} -> Void",
219
- fn_args: arg_type_map.map { |k, arg_type| "#{k} : #{arg_type[:crystal_type]}" }.join(","),
360
+ fn_args: arg_type_map
361
+ .reject { |k, _v| is_block_arg?(k) }
362
+ .map { |k, arg_type| "#{k} : #{arg_type[:crystal_type]}" }.join(","),
220
363
  fn_ret_type: return_type_map[:crystal_type],
221
364
  lib_fn_args: lib_fn_args,
222
365
  lib_fn_arg_names: lib_fn_arg_names,
@@ -224,7 +367,7 @@ module CrystalRuby
224
367
  convert_lib_args: arg_type_map.map do |k, arg_type|
225
368
  "#{k} = #{arg_type[:convert_lib_to_crystal_type]["_#{k}"]}"
226
369
  end.join("\n "),
227
- arg_names: args.keys.join(","),
370
+ arg_names: args.keys.reject(&method(:is_block_arg?)).join(", "),
228
371
  convert_return_type: return_type_map[:convert_crystal_to_lib_type]["return_value"],
229
372
  error_value: return_type_map[:error_value]
230
373
  }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CrystalRuby
2
4
  class Library
3
5
  include Typemaps
@@ -8,7 +10,8 @@ module CrystalRuby
8
10
  CR_COMPILE_MUX = Mutex.new
9
11
  CR_ATTACH_MUX = Mutex.new
10
12
 
11
- attr_accessor :name, :methods, :chunks, :root_dir, :lib_dir, :src_dir, :codegen_dir, :reactor
13
+ attr_accessor :name, :methods, :exposed_methods, :chunks, :root_dir,
14
+ :lib_dir, :src_dir, :codegen_dir, :shards
12
15
 
13
16
  @libs_by_name = {}
14
17
 
@@ -29,7 +32,9 @@ module CrystalRuby
29
32
  def initialize(name)
30
33
  self.name = name
31
34
  self.methods = {}
35
+ self.exposed_methods = {}
32
36
  self.chunks = []
37
+ self.shards = {}
33
38
  initialize_library!
34
39
  end
35
40
 
@@ -44,7 +49,7 @@ module CrystalRuby
44
49
  ].each do |dir|
45
50
  FileUtils.mkdir_p(dir)
46
51
  end
47
- IO.write main_file, "require \"./#{config.crystal_codegen_dir}/index\"\n" unless File.exist?(main_file)
52
+ IO.write main_file, "require \"./#{config.crystal_codegen_dir}/index\"\n" unless File.exist?(main_file)
48
53
 
49
54
  return if File.exist?(shard_file)
50
55
 
@@ -71,7 +76,24 @@ module CrystalRuby
71
76
  ).tap do |func|
72
77
  func.define_crystalized_methods!(self)
73
78
  func.register_custom_types!(self)
74
- write_chunk(method.owner.name, method.name, func.chunk)
79
+ write_chunk(func.owner_name, method.name, func.chunk)
80
+ end
81
+ end
82
+ end
83
+
84
+ def expose_method(method, args, returns)
85
+ CR_ATTACH_MUX.synchronize do
86
+ methods.each_value(&:unattach!)
87
+ method_key = "#{method.owner.name}/#{method.name}"
88
+ methods[method_key] = Function.new(
89
+ method: method,
90
+ args: args,
91
+ returns: returns,
92
+ ruby: true,
93
+ lib: self
94
+ ).tap do |func|
95
+ func.register_custom_types!(self)
96
+ write_chunk(func.owner_name, method.name, func.ruby_interface)
75
97
  end
76
98
  end
77
99
  end
@@ -89,7 +111,7 @@ module CrystalRuby
89
111
  end
90
112
 
91
113
  def crystalize_chunk(mod, chunk_name, body)
92
- write_chunk(mod.name, chunk_name, body)
114
+ write_chunk(mod.respond_to?(:name) ? name : "main", chunk_name, body)
93
115
  end
94
116
 
95
117
  def instantiated?
@@ -102,7 +124,15 @@ module CrystalRuby
102
124
  file_digest = Digest::MD5.hexdigest chunk_data
103
125
  fname = chunk[:chunk_name]
104
126
  index_contents.include?("#{chunk[:module_name]}/#{fname}_#{file_digest}.cr")
105
- end
127
+ end && shards_installed?
128
+ end
129
+
130
+ def shards_installed?
131
+ shard_file_content = nil
132
+ shards.all? do |k, v|
133
+ dependencies ||= shard_file_contents["dependencies"]
134
+ dependencies[k] == v
135
+ end && CrystalRuby::Compilation.shard_check?(src_dir)
106
136
  end
107
137
 
108
138
  def index_contents
@@ -112,50 +142,126 @@ module CrystalRuby
112
142
  end
113
143
 
114
144
  def register_type!(type)
115
- @types_cache ||= {}
116
- @types_cache[type.name] = type.type_defn
145
+ write_chunk("types", type.crystal_class_name, build_type(type.crystal_class_name, type.type_defn))
117
146
  end
118
147
 
119
- def type_modules
120
- (@types_cache || {}).map do |type_name, expr|
121
- parts = type_name.split("::")
122
- typedef = parts[0...-1].each_with_index.reduce("") do |acc, (part, index)|
123
- acc + "#{" " * index}module #{part}\n"
124
- end
125
- typedef += "#{" " * (parts.size - 1)}alias #{parts.last} = #{expr}\n"
126
- typedef + parts[0...-1].reverse.each_with_index.reduce("") do |acc, (_part, index)|
127
- acc + "#{" " * (parts.size - 2 - index)}end\n"
128
- end
129
- end.join("\n")
148
+ def build_type(type_name, expr)
149
+ parts = type_name.split("::")
150
+ typedef = parts[0...-1].each_with_index.reduce("") do |acc, (part, index)|
151
+ acc + "#{" " * index}module #{part}\n"
152
+ end
153
+ typedef += "#{" " * parts.size}#{expr}\n"
154
+ typedef + parts[0...-1].reverse.each_with_index.reduce("") do |acc, (_part, index)|
155
+ acc + "#{" " * (parts.size - 2 - index)}end\n"
156
+ end
157
+ end
158
+
159
+ def shard_file_contents
160
+ @shard_file_contents ||= YAML.safe_load(IO.read(shard_file))
161
+ rescue StandardError
162
+ @shard_file_contents ||= { "name" => "src", "version" => "0.1.0", "dependencies" => {} }
163
+ end
164
+
165
+ def shard_file_contents=(contents)
166
+ IO.write(shard_file, JSON.load(contents.to_json).to_yaml)
167
+ end
168
+
169
+ def shard_dependencies
170
+ shard_file_contents["dependencies"] ||= {}
171
+ end
172
+
173
+ def require_shard(name, opts)
174
+ @shards[name.to_s] = JSON.parse(opts.merge("_crystalruby_managed" => true).to_json)
175
+ rewrite_shards_file!
176
+ end
177
+
178
+ def rewrite_shards_file!
179
+ dependencies = shard_dependencies
180
+
181
+ dirty = @shards.any? do |k, v|
182
+ dependencies[k] != v
183
+ end || (@shards.empty? && dependencies.any?)
184
+
185
+ return unless dirty
186
+
187
+ if @shards.empty?
188
+ shard_file_contents.delete("dependencies")
189
+ else
190
+ shard_file_contents["dependencies"] = @shards
191
+ end
192
+
193
+ self.shard_file_contents = shard_file_contents
130
194
  end
131
195
 
132
196
  def requires
133
- chunks.map do |chunk|
134
- chunk_data = chunk[:body]
135
- file_digest = Digest::MD5.hexdigest chunk_data
136
- fname = chunk[:chunk_name]
137
- "require \"./#{chunk[:module_name]}/#{fname}_#{file_digest}.cr\"\n"
138
- end.join("\n")
197
+ Template::Type.render({}) +
198
+ Template::Primitive.render({}) +
199
+ Template::FixedWidth.render({}) +
200
+ Template::VariableWidth.render({}) +
201
+ chunks.map do |chunk|
202
+ chunk_data = chunk[:body]
203
+ file_digest = Digest::MD5.hexdigest chunk_data
204
+ fname = chunk[:chunk_name]
205
+ "require \"./#{chunk[:module_name]}/#{fname}_#{file_digest}.cr\"\n"
206
+ end.join("\n") + shards.keys.map do |shard_name|
207
+ "require \"#{shard_name}\"\n"
208
+ end.join("\n")
139
209
  end
140
210
 
141
211
  def build!
142
212
  CR_COMPILE_MUX.synchronize do
143
- File.write codegen_dir / "index.cr", Template::Index.render(
144
- type_modules: type_modules,
145
- requires: requires
146
- )
147
-
213
+ File.write codegen_dir / "index.cr", Template::Index.render(requires: requires)
148
214
  unless compiled?
215
+ FileUtils.rm_f(lib_file)
216
+
217
+ if shard_dependencies.any? && shards.empty?
218
+ rewrite_shards_file!
219
+ end
220
+
221
+ CrystalRuby::Compilation.install_shards!(src_dir)
149
222
  CrystalRuby::Compilation.compile!(
150
223
  verbose: config.verbose,
151
224
  debug: config.debug,
152
225
  src: main_file,
153
- lib: lib_file
226
+ lib: "#{lib_file}.part"
154
227
  )
228
+ FileUtils.mv("#{lib_file}.part", lib_file)
229
+ attach!
155
230
  end
156
231
  end
157
232
  end
158
233
 
234
+ def attach!
235
+ CR_ATTACH_MUX.synchronize do
236
+ lib_file = self.lib_file
237
+ lib_methods = methods
238
+ lib_methods.values.reject(&:ruby).each(&:attach_ffi_func!)
239
+ singleton_class.class_eval do
240
+ extend FFI::Library
241
+ ffi_lib lib_file
242
+ %i[yield init].each do |method_name|
243
+ singleton_class.undef_method(method_name) if singleton_class.method_defined?(method_name)
244
+ undef_method(method_name) if method_defined?(method_name)
245
+ end
246
+ attach_function :init, %i[string pointer pointer], :void
247
+ attach_function :yield, %i[], :int
248
+ lib_methods.each_value.select(&:ruby).each do |method|
249
+ attach_function :"register_#{method.name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback", %i[pointer], :void
250
+ end
251
+ end
252
+
253
+ if CrystalRuby.config.single_thread_mode
254
+ Reactor.init_single_thread_mode!
255
+ else
256
+ Reactor.start!
257
+ end
258
+
259
+ Reactor.schedule_work!(self, :init, name, Reactor::ERROR_CALLBACK, Types::Type::ARC_MUTEX.to_ptr, :void,
260
+ blocking: true, async: false)
261
+ methods.values.select(&:ruby).each(&:register_callback!)
262
+ end
263
+ end
264
+
159
265
  def digest
160
266
  Digest::MD5.hexdigest(File.read(codegen_dir / "index.cr")) if File.exist?(codegen_dir / "index.cr")
161
267
  end
@@ -166,30 +272,29 @@ module CrystalRuby
166
272
 
167
273
  def write_chunk(module_name, chunk_name, body)
168
274
  chunks.delete_if { |chnk| chnk[:module_name] == module_name && chnk[:chunk_name] == chunk_name }
169
- chunks << { module_name: module_name, chunk_name: chunk_name, body: body }
275
+ chunk = { module_name: module_name, chunk_name: chunk_name, body: body }
276
+ chunks << chunk
170
277
  existing = Dir.glob(codegen_dir / "**/*.cr")
171
278
 
172
279
  current_index_contents = index_contents
173
- chunks.each do |chunk|
174
- module_name, chunk_name, body = chunk.values_at(:module_name, :chunk_name, :body)
280
+ module_name, chunk_name, body = chunk.values_at(:module_name, :chunk_name, :body)
175
281
 
176
- file_digest = Digest::MD5.hexdigest body
177
- filename = (codegen_dir / module_name / "#{chunk_name}_#{file_digest}.cr").to_s
282
+ file_digest = Digest::MD5.hexdigest body
283
+ filename = (codegen_dir / module_name / "#{chunk_name}_#{file_digest}.cr").to_s
178
284
 
179
- unless current_index_contents.include?("#{module_name}/#{chunk_name}_#{file_digest}.cr")
180
- methods.each_value(&:unattach!)
181
- @compiled = false
182
- end
285
+ unless current_index_contents.include?("#{module_name}/#{chunk_name}_#{file_digest}.cr")
286
+ methods.each_value(&:unattach!)
287
+ @compiled = false
288
+ end
183
289
 
184
- unless existing.delete(filename)
185
- FileUtils.mkdir_p(codegen_dir / module_name)
186
- File.write(filename, body)
187
- end
188
- existing.select do |f|
189
- f =~ /#{config.crystal_codegen_dir / module_name / "#{chunk_name}_[a-f0-9]{32}\.cr"}/
190
- end.each do |fl|
191
- File.delete(fl) unless fl.eql?(filename)
192
- end
290
+ unless existing.delete(filename)
291
+ FileUtils.mkdir_p(codegen_dir / module_name)
292
+ File.write(filename, body)
293
+ end
294
+ existing.select do |f|
295
+ f =~ /#{config.crystal_codegen_dir / module_name / "#{chunk_name}_[a-f0-9]{32}\.cr"}/
296
+ end.each do |fl|
297
+ File.delete(fl) unless fl.eql?(filename)
193
298
  end
194
299
  end
195
300
  end