crystalruby 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/README.md +391 -195
  4. data/Rakefile +4 -3
  5. data/crystalruby.gemspec +2 -2
  6. data/exe/crystalruby +1 -0
  7. data/lib/crystalruby/adapter.rb +131 -65
  8. data/lib/crystalruby/arc_mutex.rb +47 -0
  9. data/lib/crystalruby/compilation.rb +33 -4
  10. data/lib/crystalruby/config.rb +41 -37
  11. data/lib/crystalruby/function.rb +211 -68
  12. data/lib/crystalruby/library.rb +153 -48
  13. data/lib/crystalruby/reactor.rb +40 -23
  14. data/lib/crystalruby/source_reader.rb +86 -0
  15. data/lib/crystalruby/template.rb +16 -5
  16. data/lib/crystalruby/templates/function.cr +11 -10
  17. data/lib/crystalruby/templates/index.cr +53 -66
  18. data/lib/crystalruby/templates/inline_chunk.cr +1 -1
  19. data/lib/crystalruby/templates/ruby_interface.cr +34 -0
  20. data/lib/crystalruby/templates/top_level_function.cr +62 -0
  21. data/lib/crystalruby/templates/top_level_ruby_interface.cr +33 -0
  22. data/lib/crystalruby/typebuilder.rb +11 -55
  23. data/lib/crystalruby/typemaps.rb +92 -67
  24. data/lib/crystalruby/types/concerns/allocator.rb +80 -0
  25. data/lib/crystalruby/types/fixed_width/named_tuple.cr +80 -0
  26. data/lib/crystalruby/types/fixed_width/named_tuple.rb +86 -0
  27. data/lib/crystalruby/types/fixed_width/proc.cr +45 -0
  28. data/lib/crystalruby/types/fixed_width/proc.rb +79 -0
  29. data/lib/crystalruby/types/fixed_width/tagged_union.cr +53 -0
  30. data/lib/crystalruby/types/fixed_width/tagged_union.rb +109 -0
  31. data/lib/crystalruby/types/fixed_width/tuple.cr +82 -0
  32. data/lib/crystalruby/types/fixed_width/tuple.rb +92 -0
  33. data/lib/crystalruby/types/fixed_width.cr +138 -0
  34. data/lib/crystalruby/types/fixed_width.rb +205 -0
  35. data/lib/crystalruby/types/primitive.cr +21 -0
  36. data/lib/crystalruby/types/primitive.rb +117 -0
  37. data/lib/crystalruby/types/primitive_types/bool.cr +34 -0
  38. data/lib/crystalruby/types/primitive_types/bool.rb +11 -0
  39. data/lib/crystalruby/types/primitive_types/nil.cr +35 -0
  40. data/lib/crystalruby/types/primitive_types/nil.rb +16 -0
  41. data/lib/crystalruby/types/primitive_types/numbers.cr +37 -0
  42. data/lib/crystalruby/types/primitive_types/numbers.rb +28 -0
  43. data/lib/crystalruby/types/primitive_types/symbol.cr +55 -0
  44. data/lib/crystalruby/types/primitive_types/symbol.rb +35 -0
  45. data/lib/crystalruby/types/primitive_types/time.cr +35 -0
  46. data/lib/crystalruby/types/primitive_types/time.rb +25 -0
  47. data/lib/crystalruby/types/type.cr +64 -0
  48. data/lib/crystalruby/types/type.rb +239 -30
  49. data/lib/crystalruby/types/variable_width/array.cr +74 -0
  50. data/lib/crystalruby/types/variable_width/array.rb +88 -0
  51. data/lib/crystalruby/types/variable_width/hash.cr +146 -0
  52. data/lib/crystalruby/types/variable_width/hash.rb +117 -0
  53. data/lib/crystalruby/types/variable_width/string.cr +36 -0
  54. data/lib/crystalruby/types/variable_width/string.rb +18 -0
  55. data/lib/crystalruby/types/variable_width.cr +23 -0
  56. data/lib/crystalruby/types/variable_width.rb +46 -0
  57. data/lib/crystalruby/types.rb +32 -13
  58. data/lib/crystalruby/version.rb +2 -2
  59. data/lib/crystalruby.rb +13 -6
  60. metadata +41 -19
  61. data/lib/crystalruby/types/array.rb +0 -15
  62. data/lib/crystalruby/types/bool.rb +0 -3
  63. data/lib/crystalruby/types/hash.rb +0 -17
  64. data/lib/crystalruby/types/named_tuple.rb +0 -28
  65. data/lib/crystalruby/types/nil.rb +0 -3
  66. data/lib/crystalruby/types/numbers.rb +0 -5
  67. data/lib/crystalruby/types/string.rb +0 -3
  68. data/lib/crystalruby/types/symbol.rb +0 -3
  69. data/lib/crystalruby/types/time.rb +0 -8
  70. data/lib/crystalruby/types/tuple.rb +0 -17
  71. data/lib/crystalruby/types/type_serializer/json.rb +0 -41
  72. data/lib/crystalruby/types/type_serializer.rb +0 -37
  73. data/lib/crystalruby/types/typedef.rb +0 -57
  74. data/lib/crystalruby/types/union_type.rb +0 -43
  75. data/lib/module.rb +0 -3
@@ -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