Class: CrystalRuby::Function

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Config, Typemaps
Defined in:
lib/crystalruby/function.rb

Overview

This class represents a single Crystalized function. Each such function belongs a shared lib (See: CrystalRuby::Library) and is attached to a single owner (a class or a module).

Constant Summary

Constants included from Typemaps

Typemaps::CRYSTAL_TYPE_MAP, Typemaps::C_TYPE_CONVERSIONS, Typemaps::C_TYPE_MAP, Typemaps::ERROR_VALUE, Typemaps::FFI_TYPE_MAP

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Config

#config

Methods included from Typemaps

#build_type_map, #convert_crystal_to_lib_type, #convert_lib_to_crystal_type, #crystal_type, #error_value, #ffi_type, #lib_type

Constructor Details

#initialize(method:, args:, returns:, lib:, function_body: nil, async: false, ruby: false, &block) ⇒ Function

Returns a new instance of Function.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/crystalruby/function.rb', line 17

def initialize(method:, args:, returns:, lib:, function_body: nil, async: false, ruby: false, &block)
  self.original_method = method
  self.owner = method.owner
  self.args = args
  self.returns = returns
  self.function_body = function_body
  self.lib = lib
  self.async = async
  self.block = block
  self.attached = false
  self.class_method = owner.singleton_class? && owner.attached_object.class == Class
  self.instance_method = original_method.is_a?(UnboundMethod) && original_method.owner.ancestors.include?(CrystalRuby::Types::Type)
  self.ruby = ruby
  self.arity = args.keys.-([:__yield_to]).size
end

Instance Attribute Details

#argsObject

Returns the value of attribute args.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def args
  @args
end

#arityObject

Returns the value of attribute arity.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def arity
  @arity
end

#asyncObject

Returns the value of attribute async.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def async
  @async
end

#attachedObject

Returns the value of attribute attached.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def attached
  @attached
end

#blockObject

Returns the value of attribute block.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def block
  @block
end

#class_methodObject

Returns the value of attribute class_method.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def class_method
  @class_method
end

#function_bodyObject

Returns the value of attribute function_body.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def function_body
  @function_body
end

#instance_methodObject

Returns the value of attribute instance_method.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def instance_method
  @instance_method
end

#libObject

Returns the value of attribute lib.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def lib
  @lib
end

#original_methodObject

Returns the value of attribute original_method.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def original_method
  @original_method
end

#ownerObject

Returns the value of attribute owner.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def owner
  @owner
end

#returnsObject

Returns the value of attribute returns.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def returns
  @returns
end

#rubyObject

Returns the value of attribute ruby.



12
13
14
# File 'lib/crystalruby/function.rb', line 12

def ruby
  @ruby
end

Instance Method Details

#arg_mapsObject



226
227
228
# File 'lib/crystalruby/function.rb', line 226

def arg_maps
  @arg_maps ||= arg_type_map.map { |_k, arg_type| arg_type[:arg_mapper] }
end

#arg_type_mapObject



184
185
186
# File 'lib/crystalruby/function.rb', line 184

def arg_type_map
  @arg_type_map ||= args.transform_values(&method(:build_type_map))
end

#arg_unmapsObject



230
231
232
# File 'lib/crystalruby/function.rb', line 230

def arg_unmaps
  @arg_unmaps ||= arg_type_map.reject { |k, _v| is_block_arg?(k) }.map { |_k, arg_type| arg_type[:retval_mapper] }
end

#attach_ffi_func!Object

Attaches the crystalized FFI functions to their related Ruby modules and classes. If a wrapper block has been passed to the crystalize function, then the we also wrap the crystalized function using a prepended Module.



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/crystalruby/function.rb', line 115

def attach_ffi_func!
  argtypes = ffi_types
  rettype = ffi_ret_type
  if async && !config.single_thread_mode
    argtypes += %i[int pointer]
    rettype = :void
  end

  owner.extend FFI::Library unless owner.is_a?(FFI::Library)

  unless (owner.instance_variable_get(:@ffi_libs) || [])
         .map(&:name)
         .map(&File.method(:basename))
         .include?(File.basename(lib.lib_file))
    owner.ffi_lib lib.lib_file
  end

  if owner.method_defined?(ffi_name)
    owner.undef_method(ffi_name)
    owner.singleton_class.undef_method(ffi_name)
  end

  owner.attach_function ffi_name, argtypes, rettype, blocking: true
  around_wrapper_block = block
  method_name = name
  @attached = true
  return unless around_wrapper_block

  @around_wrapper ||= begin
    wrapper_module = Module.new {}
    [owner, owner.singleton_class].each do |receiver|
      receiver.prepend(wrapper_module)
    end
    wrapper_module
  end
  @around_wrapper.undef_method(method_name) if @around_wrapper.method_defined?(method_name)
  @around_wrapper.define_method(method_name, &around_wrapper_block)
end

#attached?Boolean

Returns:

  • (Boolean)


158
159
160
# File 'lib/crystalruby/function.rb', line 158

def attached?
  @attached
end

#chunkObject



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/crystalruby/function.rb', line 344

def chunk
  template = owner == Object ? Template::TopLevelFunction : Template::Function
  @chunk ||= template.render(
    {
      module_or_class: instance_method || class_method ? "class" : "module",
      receiver: instance_method ? "#{owner_name}.new(_self)" : owner_name,
      fn_scope: instance_method ? "" : "self.",
      superclass: instance_method || class_method ? "< #{crystal_supertype}" : nil,
      module_name: owner_name,
      lib_fn_name: lib_fn_name,
      fn_name: name,
      callback_name: "#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback",
      fn_body: function_body,
      block_converter: takes_block? ? arg_type_map[:__yield_to][:crystalruby_type].block_converter : "",
      callback_call: returns == :void ? "callback.call(thread_id)" : "callback.call(thread_id, converted)",
      callback_type: return_type_map[:ffi_type] == :void ? "UInt32 -> Void" : " UInt32, #{return_type_map[:lib_type]} -> Void",
      fn_args: arg_type_map
        .reject { |k, _v| is_block_arg?(k) }
        .map { |k, arg_type| "#{k} : #{arg_type[:crystal_type]}" }.join(","),
      fn_ret_type: return_type_map[:crystal_type],
      lib_fn_args: lib_fn_args,
      lib_fn_arg_names: lib_fn_arg_names,
      lib_fn_ret_type: return_type_map[:lib_type],
      convert_lib_args: arg_type_map.map do |k, arg_type|
        "#{k} = #{arg_type[:convert_lib_to_crystal_type]["_#{k}"]}"
      end.join("\n    "),
      arg_names: args.keys.reject(&method(:is_block_arg?)).join(", "),
      convert_return_type: return_type_map[:convert_crystal_to_lib_type]["return_value"],
      error_value: return_type_map[:error_value]
    }
  )
end

#crystal_supertypeObject



33
34
35
36
37
# File 'lib/crystalruby/function.rb', line 33

def crystal_supertype
  return nil unless original_method.owner.ancestors.include?(CrystalRuby::Types::Type)

  original_method.owner.crystal_supertype
end

#custom_typesObject



238
239
240
241
242
243
244
# File 'lib/crystalruby/function.rb', line 238

def custom_types
  @custom_types ||= begin
    types = [*arg_type_map.values, return_type_map].map { |t| t[:crystalruby_type] }
    types.unshift(owner) if instance_method
    types
  end
end

#define_crystalized_methods!(lib) ⇒ Object

This is where we write/overwrite the class and instance methods with their crystalized equivalents. We also perform JIT compilation and JIT attachment of the FFI functions. Crystalized methods can be redefined without restarting, if running in a live-reloading environment. If they are redefined with a different function body, the new function body will result in a new digest and the FFI function will be recompiled and reattached.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/crystalruby/function.rb', line 45

def define_crystalized_methods!(lib)
  func = self
  receivers = instance_method ? [owner] : [owner, owner.singleton_class]
  receivers.each do |receiver|
    receiver.undef_method(name) if receiver.method_defined?(name)
    receiver.define_method(name) do |*args, &blk|
      unless func.attached?
        should_reenter = func.unwrapped?
        lib.build! unless lib.compiled?
        lib.attach! unless func.attached?
        return send(func.name, *args, &blk) if should_reenter
      end
      # All crystalruby functions are executed on the reactor to ensure Crystal/Ruby interop code is executed
      # from a single same thread. (Needed to make GC and Fiber scheduler happy)
      # Type mapping (if required) is applied on arguments and on return values.
      if args.length != func.arity
        raise ArgumentError,
              "wrong number of arguments (given #{args.length}, expected #{func.arity})"
      end

      raise ArgumentError, "block given but function does not accept block" if blk && !func.takes_block?
      raise ArgumentError, "no block given but function expects block" if !blk && func.takes_block?

      args << blk if blk
      func.map_args!(args)
      args.unshift(memory) if func.instance_method

      ret_val = Reactor.schedule_work!(
        func.owner,
        func.ffi_name,
        *args,
        func.ffi_ret_type,
        async: func.async,
        lib: lib
      )

      func.map_retval(ret_val)
    end
  end
end

#ffi_nameObject



174
175
176
# File 'lib/crystalruby/function.rb', line 174

def ffi_name
  lib_fn_name + (async && !config.single_thread_mode ? "_async" : "")
end

#ffi_ret_typeObject



234
235
236
# File 'lib/crystalruby/function.rb', line 234

def ffi_ret_type
  @ffi_ret_type ||= return_type_map[:ffi_ret_type]
end

#ffi_typesObject



218
219
220
221
222
223
224
# File 'lib/crystalruby/function.rb', line 218

def ffi_types
  @ffi_types ||= begin
    ffi_types = arg_type_map.map { |_k, arg_type| arg_type[:ffi_type] }
    ffi_types.unshift(:pointer) if instance_method
    ffi_types
  end
end

#is_block_arg?(arg_name) ⇒ Boolean

Returns:

  • (Boolean)


305
306
307
308
309
# File 'lib/crystalruby/function.rb', line 305

def is_block_arg?(arg_name)
  arg_name == :__yield_to && arg_type_map[arg_name] && arg_type_map[arg_name][:crystalruby_type].ancestors.select do |a|
    a < Types::Type
  end.map(&:typename).any?(:Proc)
end

#lib_fn_arg_names(skip_blocks = false) ⇒ Object



198
199
200
201
202
203
204
# File 'lib/crystalruby/function.rb', line 198

def lib_fn_arg_names(skip_blocks = false)
  @lib_fn_arg_names ||= begin
    names = arg_type_map.keys.reject { |k, _v| skip_blocks && is_block_arg?(k) }.map { |k| "_#{k}" }
    names.unshift("self.memory") if instance_method
    names.join(",") + (names.empty? ? "" : ", ")
  end
end

#lib_fn_argsObject



188
189
190
191
192
193
194
195
196
# File 'lib/crystalruby/function.rb', line 188

def lib_fn_args
  @lib_fn_args ||= begin
    lib_fn_args = arg_type_map.map do |k, arg_type|
      "_#{k} : #{arg_type[:lib_type]}"
    end
    lib_fn_args.unshift("_self : Pointer(::UInt8)") if instance_method
    lib_fn_args.join(",") + (lib_fn_args.empty? ? "" : ", ")
  end
end

#lib_fn_nameObject



178
179
180
181
182
# File 'lib/crystalruby/function.rb', line 178

def lib_fn_name
  @lib_fn_name ||= "#{owner_name.downcase.gsub("::",
                                               "_")}_#{name.to_s.gsub("?", "query").gsub("!", "bang").gsub("=",
                                                                                                           "eq")}_#{Digest::MD5.hexdigest(function_body.to_s)}"
end

#lib_fn_typesObject



206
207
208
209
210
211
212
# File 'lib/crystalruby/function.rb', line 206

def lib_fn_types
  @lib_fn_types ||= begin
    lib_fn_types = arg_type_map.map { |_k, v| v[:lib_type] }
    lib_fn_types.unshift("Pointer(::UInt8)") if instance_method
    lib_fn_types.join(",") + (lib_fn_types.empty? ? "" : ", ")
  end
end

#map_args!(args) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/crystalruby/function.rb', line 256

def map_args!(args)
  return args unless arg_maps.any?

  refs = nil

  arg_maps.each_with_index do |argmap, index|
    next unless argmap

    mapped = argmap[args[index]]
    case mapped
    when CrystalRuby::Types::Type then
      args[index] = mapped.memory
      (refs ||= []) << mapped
    else
      args[index] = mapped
    end
  end
  refs
end

#map_retval(retval) ⇒ Object



287
288
289
290
291
# File 'lib/crystalruby/function.rb', line 287

def map_retval(retval)
  return retval unless return_type_map[:retval_mapper]

  return_type_map[:retval_mapper][retval]
end

#owner_nameObject



170
171
172
# File 'lib/crystalruby/function.rb', line 170

def owner_name
  owner.name
end

#register_callback!Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/crystalruby/function.rb', line 86

def register_callback!
  return unless ruby

  @callback_func = FFI::Function.new(ffi_ret_type, ffi_types) do |*args|
    receiver = instance_method ? owner.new(args.shift) : owner
    ret_val = if takes_block?
                block_arg = arg_type_map[:__yield_to][:crystalruby_type].new(args.pop)
                receiver.send(name, *unmap_args(args)) do |*args|
                  args = args.map.with_index do |arg, i|
                    arg = block_arg.inner_types[i].new(arg) unless arg.is_a?(block_arg.inner_types[i])
                    arg.memory
                  end
                  return_val = block_arg.invoke(*args)
                  unless return_val.is_a?(block_arg.inner_types[-1])
                    return_val = block_arg.inner_types[-1].new(return_val)
                  end
                  block_arg.inner_types[-1].anonymous? ? return_val.value : return_val
                end
              else
                receiver.send(name, *unmap_args(args))
              end
    unmap_retval(ret_val)
  end
  Reactor.schedule_work!(lib, :"register_#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback", @callback_func, :void, blocking: true, async: false)
end

#register_custom_types!(lib) ⇒ Object



246
247
248
249
250
251
252
253
254
# File 'lib/crystalruby/function.rb', line 246

def register_custom_types!(lib)
  custom_types.each do |crystalruby_type|
    next unless crystalruby_type.is_a?(Class) && crystalruby_type < Types::Type

    [*crystalruby_type.nested_types].uniq.each do |type|
      lib.register_type!(type)
    end
  end
end

#return_type_mapObject



214
215
216
# File 'lib/crystalruby/function.rb', line 214

def return_type_map
  @return_type_map ||= build_type_map(returns)
end

#ruby_interfaceObject



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/crystalruby/function.rb', line 311

def ruby_interface
  template = owner == Object ? Template::TopLevelRubyInterface : Template::RubyInterface
  @ruby_interface ||= template.render(
    {
      module_or_class: instance_method || class_method ? "class" : "module",
      receiver: instance_method ? "#{owner_name}.new(_self)" : owner_name,
      fn_scope: instance_method ? "" : "self.",
      superclass: instance_method || class_method ? "< #{crystal_supertype}" : nil,
      module_name: owner_name,
      lib_fn_name: lib_fn_name,
      fn_name: name,
      callback_name: "#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback",
      fn_body: function_body,
      block_converter: takes_block? ? arg_type_map[:__yield_to][:crystalruby_type].block_converter : "",
      callback_call: returns == :void ? "callback.call(thread_id)" : "callback.call(thread_id, converted)",
      callback_type: return_type_map[:ffi_type] == :void ? "UInt32 -> Void" : " UInt32, #{return_type_map[:lib_type]} -> Void",
      fn_args: arg_type_map
          .map { |k, arg_type| "#{is_block_arg?(k) ? "&" : ""}#{k} : #{arg_type[:crystal_type]}" }.join(","),
      fn_ret_type: return_type_map[:crystal_type],
      lib_fn_args: lib_fn_args,
      lib_fn_types: lib_fn_types,
      lib_fn_arg_names: lib_fn_arg_names,
      lib_fn_ret_type: return_type_map[:lib_type],
      convert_lib_args: arg_type_map.map do |k, arg_type|
                          "_#{k} = #{arg_type[:convert_crystal_to_lib_type]["#{k}"]}"
                        end.join("\n    "),
      arg_names: args.keys.reject(&method(:is_block_arg?)).join(", "),
      convert_return_type: return_type_map[:convert_lib_to_crystal_type]["return_value"],
      error_value: return_type_map[:error_value]
    }
  )
end

#takes_block?Boolean

Returns:

  • (Boolean)


301
302
303
# File 'lib/crystalruby/function.rb', line 301

def takes_block?
  is_block_arg?(:__yield_to)
end

#unattach!Object



162
163
164
# File 'lib/crystalruby/function.rb', line 162

def unattach!
  @attached = false
end

#unmap_args(args) ⇒ Object



276
277
278
279
280
281
282
283
284
285
# File 'lib/crystalruby/function.rb', line 276

def unmap_args(args)
  return args unless args.any?

  arg_unmaps.each_with_index do |argmap, index|
    next unless argmap

    args[index] = argmap[args[index]]
  end
  args
end

#unmap_retval(retval) ⇒ Object



293
294
295
296
297
298
299
# File 'lib/crystalruby/function.rb', line 293

def unmap_retval(retval)
  return retval unless return_type_map[:arg_mapper]

  retval = return_type_map[:arg_mapper][retval]
  retval = retval.memory if retval.kind_of?(CrystalRuby::Types::Type)
  retval
end

#unwrapped?Boolean

Returns:

  • (Boolean)


154
155
156
# File 'lib/crystalruby/function.rb', line 154

def unwrapped?
  block && !@around_wrapper
end