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 crystalized FFI functions to their related Ruby modules and classes.
- #attached? ⇒ Boolean
- #chunk ⇒ Object
- #crystal_supertype ⇒ Object
- #custom_types ⇒ Object
-
#define_crystalized_methods!(lib) ⇒ Object
This is where we write/overwrite the class and instance methods with their crystalized 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
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_map ⇒ Object
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_unmaps ⇒ Object
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
158 159 160 |
# File 'lib/crystalruby/function.rb', line 158 def attached? @attached end |
#chunk ⇒ Object
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_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
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_name ⇒ Object
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_type ⇒ Object
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_types ⇒ Object
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
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_args ⇒ Object
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_name ⇒ Object
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_types ⇒ Object
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_name ⇒ Object
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_map ⇒ Object
214 215 216 |
# File 'lib/crystalruby/function.rb', line 214 def return_type_map @return_type_map ||= build_type_map(returns) end |
#ruby_interface ⇒ Object
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
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
154 155 156 |
# File 'lib/crystalruby/function.rb', line 154 def unwrapped? block && !@around_wrapper end |