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



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

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

#arg_type_mapObject



193
194
195
# File 'lib/crystalruby/function.rb', line 193

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

#arg_unmapsObject



239
240
241
# File 'lib/crystalruby/function.rb', line 239

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 crystallized FFI functions to their related Ruby modules and classes. If a wrapper block has been passed to the crystallize function, then the we also wrap the crystallized function using a prepended Module.



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
153
154
155
156
157
158
159
160
161
# File 'lib/crystalruby/function.rb', line 124

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)


167
168
169
# File 'lib/crystalruby/function.rb', line 167

def attached?
  @attached
end

#chunkObject



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/crystalruby/function.rb', line 355

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



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

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_crystallized_methods!(lib) ⇒ Object

This is where we write/overwrite the class and instance methods with their crystallized 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
85
# File 'lib/crystalruby/function.rb', line 45

def define_crystallized_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



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

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

#ffi_ret_typeObject



243
244
245
# File 'lib/crystalruby/function.rb', line 243

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

#ffi_typesObject



227
228
229
230
231
232
233
# File 'lib/crystalruby/function.rb', line 227

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)


316
317
318
319
320
# File 'lib/crystalruby/function.rb', line 316

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



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

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



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

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



187
188
189
190
191
# File 'lib/crystalruby/function.rb', line 187

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



215
216
217
218
219
220
221
# File 'lib/crystalruby/function.rb', line 215

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



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/crystalruby/function.rb', line 265

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
      args[index] = mapped.memory
      (refs ||= []) << mapped
    else
      args[index] = mapped
    end
  end
  refs
end

#map_retval(retval) ⇒ Object



296
297
298
299
300
# File 'lib/crystalruby/function.rb', line 296

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

  return_type_map[:retval_mapper][retval]
end

#owner_nameObject



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

def owner_name
  owner.name
end

#register_callback!Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/crystalruby/function.rb', line 87

def register_callback!
  return unless ruby

  ret_type = ffi_ret_type == :string ? :pointer : ffi_ret_type
  @callback_func = FFI::Function.new(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)
          return_val = block_arg.inner_types[-1].new(return_val) unless return_val.is_a?(block_arg.inner_types[-1])
          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



255
256
257
258
259
260
261
262
263
# File 'lib/crystalruby/function.rb', line 255

def register_custom_types!(lib)
  custom_types.each do |crystalruby_type|
    next unless Types::Type.subclass?(crystalruby_type)

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

#return_type_mapObject



223
224
225
# File 'lib/crystalruby/function.rb', line 223

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

#ruby_interfaceObject



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/crystalruby/function.rb', line 322

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)


312
313
314
# File 'lib/crystalruby/function.rb', line 312

def takes_block?
  is_block_arg?(:__yield_to)
end

#unattach!Object



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

def unattach!
  @attached = false
end

#unmap_args(args) ⇒ Object



285
286
287
288
289
290
291
292
293
294
# File 'lib/crystalruby/function.rb', line 285

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



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

def unmap_retval(retval)
  return FFI::MemoryPointer.from_string(retval) if return_type_map[:ffi_ret_type] == :string
  return retval unless return_type_map[:arg_mapper]

  retval = return_type_map[:arg_mapper][retval]

  retval = retval.memory if retval.is_a?(CrystalRuby::Types::Type)
  retval
end

#unwrapped?Boolean

Returns:

  • (Boolean)


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

def unwrapped?
  block && !@around_wrapper
end