Class: CrystalRuby::Function
- Inherits:
-
Object
- Object
- CrystalRuby::Function
- Extended by:
- Forwardable
- 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
-
#args ⇒ Object
Returns the value of attribute args.
-
#arity ⇒ Object
Returns the value of attribute arity.
-
#async ⇒ Object
Returns the value of attribute async.
-
#attached ⇒ Object
Returns the value of attribute attached.
-
#block ⇒ Object
Returns the value of attribute block.
-
#class_method ⇒ Object
Returns the value of attribute class_method.
-
#function_body ⇒ Object
Returns the value of attribute function_body.
-
#instance_method ⇒ Object
Returns the value of attribute instance_method.
-
#lib ⇒ Object
Returns the value of attribute lib.
-
#original_method ⇒ Object
Returns the value of attribute original_method.
-
#owner ⇒ Object
Returns the value of attribute owner.
-
#returns ⇒ Object
Returns the value of attribute returns.
-
#ruby ⇒ Object
Returns the value of attribute ruby.
Instance Method Summary collapse
- #arg_maps ⇒ Object
- #arg_type_map ⇒ Object
- #arg_unmaps ⇒ Object
-
#attach_ffi_func! ⇒ Object
Attaches the crystallized FFI functions to their related Ruby modules and classes.
- #attached? ⇒ Boolean
- #chunk ⇒ Object
- #crystal_supertype ⇒ Object
- #custom_types ⇒ Object
-
#define_crystallized_methods!(lib) ⇒ Object
This is where we write/overwrite the class and instance methods with their crystallized equivalents.
- #ffi_name ⇒ Object
- #ffi_ret_type ⇒ Object
- #ffi_types ⇒ Object
-
#initialize(method:, args:, returns:, lib:, function_body: nil, async: false, ruby: false, &block) ⇒ Function
constructor
A new instance of Function.
- #is_block_arg?(arg_name) ⇒ Boolean
- #lib_fn_arg_names(skip_blocks = false) ⇒ Object
- #lib_fn_args ⇒ Object
- #lib_fn_name ⇒ Object
- #lib_fn_types ⇒ Object
- #map_args!(args) ⇒ Object
- #map_retval(retval) ⇒ Object
- #owner_name ⇒ Object
- #register_callback! ⇒ Object
- #register_custom_types!(lib) ⇒ Object
- #return_type_map ⇒ Object
- #ruby_interface ⇒ Object
- #takes_block? ⇒ Boolean
- #unattach! ⇒ Object
- #unmap_args(args) ⇒ Object
- #unmap_retval(retval) ⇒ Object
- #unwrapped? ⇒ Boolean
Methods included from 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
#args ⇒ Object
Returns the value of attribute args.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def args @args end |
#arity ⇒ Object
Returns the value of attribute arity.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def arity @arity end |
#async ⇒ Object
Returns the value of attribute async.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def async @async end |
#attached ⇒ Object
Returns the value of attribute attached.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def attached @attached end |
#block ⇒ Object
Returns the value of attribute block.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def block @block end |
#class_method ⇒ Object
Returns the value of attribute class_method.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def class_method @class_method end |
#function_body ⇒ Object
Returns the value of attribute function_body.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def function_body @function_body end |
#instance_method ⇒ Object
Returns the value of attribute instance_method.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def instance_method @instance_method end |
#lib ⇒ Object
Returns the value of attribute lib.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def lib @lib end |
#original_method ⇒ Object
Returns the value of attribute original_method.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def original_method @original_method end |
#owner ⇒ Object
Returns the value of attribute owner.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def owner @owner end |
#returns ⇒ Object
Returns the value of attribute returns.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def returns @returns end |
#ruby ⇒ Object
Returns the value of attribute ruby.
12 13 14 |
# File 'lib/crystalruby/function.rb', line 12 def ruby @ruby end |
Instance Method Details
#arg_maps ⇒ Object
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_map ⇒ Object
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_unmaps ⇒ Object
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
167 168 169 |
# File 'lib/crystalruby/function.rb', line 167 def attached? @attached end |
#chunk ⇒ Object
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_supertype ⇒ Object
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_types ⇒ Object
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_name ⇒ Object
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_type ⇒ Object
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_types ⇒ Object
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
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_args ⇒ Object
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_name ⇒ Object
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_types ⇒ Object
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_name ⇒ Object
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_map ⇒ Object
223 224 225 |
# File 'lib/crystalruby/function.rb', line 223 def return_type_map @return_type_map ||= build_type_map(returns) end |
#ruby_interface ⇒ Object
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
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
163 164 165 |
# File 'lib/crystalruby/function.rb', line 163 def unwrapped? block && !@around_wrapper end |