crystalruby 0.1.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,228 @@
1
+ module CrystalRuby
2
+ # This class represents a single Crystalized function.
3
+ # Each such function belongs a shared lib (See: CrystalRuby::Library)
4
+ # and is attached to a single owner (a class or a module).
5
+ class Function
6
+ include Typemaps
7
+ include Config
8
+
9
+ attr_accessor :owner, :method_name, :args, :returns, :function_body, :lib, :async, :block
10
+
11
+ def initialize(method:, args:, returns:, function_body:, lib:, async: false, &block)
12
+ self.owner = method.owner
13
+ self.method_name = method.name
14
+ self.args = args
15
+ self.returns = returns
16
+ self.function_body = function_body
17
+ self.lib = lib
18
+ self.async = async
19
+ self.block = block
20
+ end
21
+
22
+ # This is where we write/overwrite the class and instance methods
23
+ # with their crystalized equivalents.
24
+ # We also perform JIT compilation and JIT attachment of the FFI functions.
25
+ # Crystalized methods work in a live-reloading manner environment.
26
+ # If they are redefined with a different function body, the new function body
27
+ # will result in a new digest and the FFI function will be recompiled and reattached.
28
+ def define_crystalized_methods!(lib)
29
+ func = self
30
+ [owner, owner.singleton_class].each do |receiver|
31
+ receiver.undef_method(method_name) if receiver.method_defined?(method_name)
32
+ receiver.define_method(method_name) do |*args|
33
+ unless lib.compiled?
34
+ lib.build!
35
+ return send(func.method_name, *args)
36
+ end
37
+ unless lib.attached?(func.owner)
38
+ should_reenter = func.attach_ffi_lib_functions!
39
+ return send(func.method_name, *args) if should_reenter
40
+ end
41
+ # All crystalruby functions are executed on the reactor to ensure
42
+ # all Crystal interop is executed from the same thread. (Needed to make GC and Fiber scheduler happy)
43
+ # Type mapping (if required) is applied on arguments and on return values.
44
+ func.map_retval(
45
+ Reactor.schedule_work!(
46
+ func.owner,
47
+ func.ffi_name,
48
+ *func.map_args(args),
49
+ func.ffi_ret_type,
50
+ async: func.async,
51
+ lib: lib
52
+ )
53
+ )
54
+ end
55
+ end
56
+ end
57
+
58
+ # This is where we attach the top-level FFI functions of the shared object
59
+ # to our library (yield and init) needed for successful operation of the reactor.
60
+ # We also initialize the shared object (needed to start the GC) and
61
+ # start the reactor unless we are in single-thread mode.
62
+ def attach_ffi_lib_functions!
63
+ should_reenter = unwrapped?
64
+ lib_file = lib.lib_file
65
+ lib.attachments[owner] = true
66
+ lib.methods.each_value do |method|
67
+ method.attach_ffi_func!
68
+ end
69
+ lib.singleton_class.class_eval do
70
+ extend FFI::Library
71
+ ffi_lib lib_file
72
+ %i[yield init].each do |method_name|
73
+ singleton_class.undef_method(method_name) if singleton_class.method_defined?(method_name)
74
+ undef_method(method_name) if method_defined?(method_name)
75
+ end
76
+ attach_function :init, %i[string pointer], :void
77
+ attach_function :yield, %i[], :int
78
+ end
79
+
80
+ if CrystalRuby.config.single_thread_mode
81
+ Reactor.init_single_thread_mode!
82
+ else
83
+ Reactor.start!
84
+ end
85
+ Reactor.schedule_work!(lib, :init, lib.name, Reactor::ERROR_CALLBACK, :void, blocking: true, async: false)
86
+ should_reenter
87
+ end
88
+
89
+ # This is where we attach the crystalized FFI functions to their related
90
+ # Ruby modules and classes. If a wrapper block has been passed to the crystalize function,
91
+ # then the we also wrap the crystalized function using a prepended Module.
92
+ def attach_ffi_func!
93
+ argtypes = ffi_types
94
+ rettype = ffi_ret_type
95
+ if async && !config.single_thread_mode
96
+ argtypes += %i[int pointer]
97
+ rettype = :void
98
+ end
99
+
100
+ owner.extend FFI::Library unless owner.is_a?(FFI::Library)
101
+
102
+ unless (owner.instance_variable_get(:@ffi_libs) || [])
103
+ .map(&:name)
104
+ .map(&File.method(:basename))
105
+ .include?(File.basename(lib.lib_file))
106
+ owner.ffi_lib lib.lib_file
107
+ end
108
+
109
+ if owner.method_defined?(ffi_name)
110
+ owner.undef_method(ffi_name)
111
+ owner.singleton_class.undef_method(ffi_name)
112
+ end
113
+
114
+ owner.attach_function ffi_name, argtypes, rettype, blocking: true
115
+ around_wrapper_block = block
116
+ method_name = self.method_name
117
+
118
+ return unless around_wrapper_block
119
+
120
+ @around_wrapper ||= begin
121
+ wrapper_module = Module.new {}
122
+ [owner, owner.singleton_class].each do |receiver|
123
+ receiver.prepend(wrapper_module)
124
+ end
125
+ wrapper_module
126
+ end
127
+ @around_wrapper.undef_method(method_name) if @around_wrapper.method_defined?(method_name)
128
+ @around_wrapper.define_method(method_name, &around_wrapper_block)
129
+ rescue StandardError => e
130
+ CrystalRuby.log_error("Error attaching #{method_name} as #{ffi_name} to #{owner.name}")
131
+ CrystalRuby.log_error(e.message)
132
+ CrystalRuby.log_error(e.backtrace.join("\n"))
133
+ end
134
+
135
+ def unwrapped?
136
+ block && !@around_wrapper
137
+ end
138
+
139
+ def ffi_name
140
+ lib_fn_name + (async && !config.single_thread_mode ? "_async" : "")
141
+ end
142
+
143
+ def lib_fn_name
144
+ @lib_fn_name ||= "#{owner.name.downcase.gsub("::", "_")}_#{method_name}_#{Digest::MD5.hexdigest(function_body)}"
145
+ end
146
+
147
+ def arg_type_map
148
+ @arg_type_map ||= args.transform_values(&method(:build_type_map))
149
+ end
150
+
151
+ def lib_fn_args
152
+ @lib_fn_args ||= arg_type_map.map { |k, arg_type|
153
+ "_#{k} : #{arg_type[:lib_type]}"
154
+ }.join(",") + (arg_type_map.empty? ? "" : ", ")
155
+ end
156
+
157
+ def lib_fn_arg_names
158
+ @lib_fn_arg_names ||= arg_type_map.map { |k, _arg_type|
159
+ "_#{k}"
160
+ }.join(",") + (arg_type_map.empty? ? "" : ", ")
161
+ end
162
+
163
+ def return_type_map
164
+ @return_type_map ||= build_type_map(returns)
165
+ end
166
+
167
+ def ffi_types
168
+ @ffi_types ||= arg_type_map.map { |_k, arg_type| arg_type[:ffi_type] }
169
+ end
170
+
171
+ def arg_maps
172
+ @arg_maps ||= arg_type_map.map { |_k, arg_type| arg_type[:arg_mapper] }
173
+ end
174
+
175
+ def ffi_ret_type
176
+ @ffi_ret_type ||= return_type_map[:ffi_ret_type]
177
+ end
178
+
179
+ def register_custom_types!(lib)
180
+ [*arg_type_map.values, return_type_map].map { |t| t[:crystal_ruby_type] }.each do |crystalruby_type|
181
+ if crystalruby_type.is_a?(Types::TypeSerializer) && !crystalruby_type.anonymous?
182
+ lib.register_type!(crystalruby_type)
183
+ end
184
+ end
185
+ end
186
+
187
+ def map_args(args)
188
+ return args unless arg_maps.any?
189
+
190
+ arg_maps.each_with_index do |argmap, index|
191
+ next unless argmap
192
+
193
+ args[index] = argmap[args[index]]
194
+ end
195
+ args
196
+ end
197
+
198
+ def map_retval(retval)
199
+ return retval unless return_type_map[:retval_mapper]
200
+
201
+ return_type_map[:retval_mapper][retval]
202
+ end
203
+
204
+ def chunk
205
+ @chunk ||= Template::Function.render(
206
+ {
207
+ module_name: owner.name,
208
+ lib_fn_name: lib_fn_name,
209
+ fn_name: method_name,
210
+ fn_body: function_body,
211
+ callback_call: returns == :void ? "callback.call(thread_id)" : "callback.call(thread_id, converted)",
212
+ callback_type: return_type_map[:ffi_type] == :void ? "UInt32 -> Void" : " UInt32, #{return_type_map[:lib_type]} -> Void",
213
+ fn_args: arg_type_map.map { |k, arg_type| "#{k} : #{arg_type[:crystal_type]}" }.join(","),
214
+ fn_ret_type: return_type_map[:crystal_type],
215
+ lib_fn_args: lib_fn_args,
216
+ lib_fn_arg_names: lib_fn_arg_names,
217
+ lib_fn_ret_type: return_type_map[:lib_type],
218
+ convert_lib_args: arg_type_map.map do |k, arg_type|
219
+ "#{k} = #{arg_type[:convert_lib_to_crystal_type]["_#{k}"]}"
220
+ end.join("\n "),
221
+ arg_names: args.keys.join(","),
222
+ convert_return_type: return_type_map[:convert_crystal_to_lib_type]["return_value"],
223
+ error_value: return_type_map[:error_value]
224
+ }
225
+ )
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,198 @@
1
+ module CrystalRuby
2
+ class Library
3
+ include Typemaps
4
+ include Config
5
+
6
+ # *CR_ATTACH_MUX* and *CR_COMPILE_MUX* are used to only allow a single FFI compile or attach operation at once
7
+ # to avoid a rare scenario where the same function is attached simultaneously across two or more threads.
8
+ CR_COMPILE_MUX = Mutex.new
9
+ CR_ATTACH_MUX = Mutex.new
10
+
11
+ attr_accessor :name, :methods, :chunks, :root_dir, :lib_dir, :src_dir, :codegen_dir, :attachments, :reactor
12
+
13
+ @libs_by_name = {}
14
+
15
+ def self.all
16
+ @libs_by_name.values
17
+ end
18
+
19
+ def self.[](name)
20
+ @libs_by_name[name] ||= begin
21
+ CrystalRuby.initialize_crystal_ruby! unless CrystalRuby.initialized?
22
+ Library.new(name)
23
+ end
24
+ end
25
+
26
+ # A Library represents a single Crystal shared object.
27
+ # It holds code as either methods (invokable from Ruby and attached) or
28
+ # anonymous chunks, which are just raw Crystal code.
29
+ def initialize(name)
30
+ self.name = name
31
+ self.methods = {}
32
+ self.chunks = []
33
+ self.attachments = Hash.new(false)
34
+ initialize_library!
35
+ end
36
+
37
+ # This method is used to
38
+ # bootstrap a library filesystem,
39
+ # and generate a top level index.cr and shard file if
40
+ # these do not already exist.
41
+ def initialize_library!
42
+ @root_dir, @lib_dir, @src_dir, @codegen_dir = [
43
+ config.crystal_src_dir_abs / name,
44
+ config.crystal_src_dir_abs / name / "lib",
45
+ config.crystal_src_dir_abs / name / "src",
46
+ config.crystal_src_dir_abs / name / "src" / config.crystal_codegen_dir
47
+ ].each do |dir|
48
+ FileUtils.mkdir_p(dir)
49
+ end
50
+ IO.write main_file, "require \"./#{config.crystal_codegen_dir}/index\"\n" unless File.exist?(main_file)
51
+
52
+ return if File.exist?(shard_file)
53
+
54
+ IO.write(shard_file, <<~YAML)
55
+ name: src
56
+ version: 0.1.0
57
+ YAML
58
+ end
59
+
60
+ # This is where we instantiate the crystalized method as a CrystalRuby::Function
61
+ # and trigger the generation of the crystal code.
62
+ def crystalize_method(method, args, returns, function_body, async, &block)
63
+ CR_ATTACH_MUX.synchronize do
64
+ attachments.delete(method.owner)
65
+ method_key = "#{method.owner.name}/#{method.name}"
66
+ methods[method_key] = Function.new(
67
+ method: method,
68
+ args: args,
69
+ returns: returns,
70
+ function_body: function_body,
71
+ async: async,
72
+ lib: self,
73
+ &block
74
+ ).tap do |func|
75
+ func.define_crystalized_methods!(self)
76
+ func.register_custom_types!(self)
77
+ write_chunk(method.owner.name, method.name, func.chunk)
78
+ end
79
+ end
80
+ end
81
+
82
+ def main_file
83
+ src_dir / "#{name}.cr"
84
+ end
85
+
86
+ def lib_file
87
+ lib_dir / "#{name}_#{digest}"
88
+ end
89
+
90
+ def shard_file
91
+ src_dir / "shard.yml"
92
+ end
93
+
94
+ def crystalize_chunk(mod, chunk_name, body)
95
+ write_chunk(mod.name, chunk_name, body)
96
+ end
97
+
98
+ def instantiated?
99
+ @instantiated
100
+ end
101
+
102
+ def compiled?
103
+ index_contents = self.index_contents
104
+ File.exist?(lib_file) && chunks.all? do |chunk|
105
+ chunk_data = chunk[:body]
106
+ file_digest = Digest::MD5.hexdigest chunk_data
107
+ fname = chunk[:chunk_name]
108
+ index_contents.include?("#{chunk[:module_name]}/#{fname}_#{file_digest}.cr")
109
+ end
110
+ end
111
+
112
+ def index_contents
113
+ IO.read(codegen_dir / "index.cr")
114
+ rescue StandardError
115
+ ""
116
+ end
117
+
118
+ def attached?(owner)
119
+ attachments[owner]
120
+ end
121
+
122
+ def register_type!(type)
123
+ @types_cache ||= {}
124
+ @types_cache[type.name] = type.type_defn
125
+ end
126
+
127
+ def type_modules
128
+ (@types_cache || {}).map do |type_name, expr|
129
+ parts = type_name.split("::")
130
+ typedef = parts[0...-1].each_with_index.reduce("") do |acc, (part, index)|
131
+ acc + "#{" " * index}module #{part}\n"
132
+ end
133
+ typedef += "#{" " * (parts.size - 1)}alias #{parts.last} = #{expr}\n"
134
+ typedef + parts[0...-1].reverse.each_with_index.reduce("") do |acc, (_part, index)|
135
+ acc + "#{" " * (parts.size - 2 - index)}end\n"
136
+ end
137
+ end.join("\n")
138
+ end
139
+
140
+ def requires
141
+ chunks.map do |chunk|
142
+ chunk_data = chunk[:body]
143
+ file_digest = Digest::MD5.hexdigest chunk_data
144
+ fname = chunk[:chunk_name]
145
+ "require \"./#{chunk[:module_name]}/#{fname}_#{file_digest}.cr\"\n"
146
+ end.join("\n")
147
+ end
148
+
149
+ def build!
150
+ CR_COMPILE_MUX.synchronize do
151
+ File.write codegen_dir / "index.cr", Template::Index.render(
152
+ type_modules: type_modules,
153
+ requires: requires
154
+ )
155
+
156
+ unless compiled?
157
+ CrystalRuby::Compilation.compile!(
158
+ verbose: config.verbose,
159
+ debug: config.debug,
160
+ src: main_file,
161
+ lib: lib_file
162
+ )
163
+ end
164
+ end
165
+ end
166
+
167
+ def digest
168
+ Digest::MD5.hexdigest(File.read(codegen_dir / "index.cr")) if File.exist?(codegen_dir / "index.cr")
169
+ end
170
+
171
+ def self.chunk_store
172
+ @chunk_store ||= []
173
+ end
174
+
175
+ def write_chunk(module_name, chunk_name, body)
176
+ chunks.delete_if { |chnk| chnk[:module_name] == module_name && chnk[:chunk_name] == chunk_name }
177
+ chunks << { module_name: module_name, chunk_name: chunk_name, body: body }
178
+ existing = Dir.glob(codegen_dir / "**/*.cr")
179
+ chunks.each do |chunk|
180
+ module_name, chunk_name, body = chunk.values_at(:module_name, :chunk_name, :body)
181
+
182
+ file_digest = Digest::MD5.hexdigest body
183
+ filename = (codegen_dir / module_name / "#{chunk_name}_#{file_digest}.cr").to_s
184
+
185
+ unless existing.delete(filename)
186
+ @attached = false
187
+ FileUtils.mkdir_p(codegen_dir / module_name)
188
+ File.write(filename, body)
189
+ end
190
+ existing.select do |f|
191
+ f =~ /#{config.crystal_codegen_dir / module_name / "#{chunk_name}_[a-f0-9]{32}\.cr"}/
192
+ end.each do |fl|
193
+ File.delete(fl) unless fl.eql?(filename)
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,155 @@
1
+ module CrystalRuby
2
+ # The Reactor represents a singleton Thread
3
+ # responsible for running all Ruby/crystal interop code.
4
+ # Crystal's Fiber scheduler and GC assumes all code is run on a single thread.
5
+ # This class is responsible for multiplexing Ruby and Crystal code on a single thread,
6
+ # to allow safe invocation of Crystal code from across any number of Ruby threads.
7
+ # Functions annotated with async: true, are executed using callbacks to allow these to be multi-plexed in a non-blocking manner.
8
+
9
+ module Reactor
10
+ module_function
11
+
12
+ class SingleThreadViolation < StandardError; end
13
+
14
+ REACTOR_QUEUE = Queue.new
15
+
16
+ # We maintain a map of threads, each with a mutex, condition variable, and result
17
+ THREAD_MAP = Hash.new do |h, tid_or_thread, tid = tid_or_thread|
18
+ if tid_or_thread.is_a?(Thread)
19
+ ObjectSpace.define_finalizer(tid_or_thread) do
20
+ THREAD_MAP.delete(tid_or_thread)
21
+ THREAD_MAP.delete(tid_or_thread.object_id)
22
+ end
23
+ tid = tid_or_thread.object_id
24
+ end
25
+
26
+ h[tid] = {
27
+ mux: Mutex.new,
28
+ cond: ConditionVariable.new,
29
+ result: nil,
30
+ thread_id: tid
31
+ }
32
+ h[tid_or_thread] = h[tid] if tid_or_thread.is_a?(Thread)
33
+ end
34
+
35
+ # We memoize callbacks, once per return type
36
+ CALLBACKS_MAP = Hash.new do |h, rt|
37
+ h[rt] = FFI::Function.new(:void, [:int, *(rt == :void ? [] : [rt])]) do |tid, ret|
38
+ THREAD_MAP[tid][:error] = nil
39
+ THREAD_MAP[tid][:result] = ret
40
+ THREAD_MAP[tid][:cond].signal
41
+ end
42
+ end
43
+
44
+ ERROR_CALLBACK = FFI::Function.new(:void, %i[string string int]) do |error_type, message, tid|
45
+ error_type = error_type.to_sym
46
+ is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception)
47
+ error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
48
+ tid = tid.zero? ? Reactor.current_thread_id : tid
49
+ raise error_type.new(message) unless THREAD_MAP.key?(tid)
50
+
51
+ THREAD_MAP[tid][:error] = error_type.new(message)
52
+ THREAD_MAP[tid][:result] = nil
53
+ THREAD_MAP[tid][:cond].signal
54
+ end
55
+
56
+ def thread_conditions
57
+ THREAD_MAP[Thread.current]
58
+ end
59
+
60
+ def await_result!
61
+ mux, cond = thread_conditions.values_at(:mux, :cond)
62
+ cond.wait(mux)
63
+ raise THREAD_MAP[thread_id][:error] if THREAD_MAP[thread_id][:error]
64
+
65
+ THREAD_MAP[thread_id][:result]
66
+ end
67
+
68
+ def start!
69
+ @main_loop ||= Thread.new do
70
+ CrystalRuby.log_debug("Starting reactor")
71
+ CrystalRuby.log_debug("CrystalRuby initialized")
72
+ loop do
73
+ REACTOR_QUEUE.pop[]
74
+ end
75
+ rescue StandardError => e
76
+ CrystalRuby.log_error "Error: #{e}"
77
+ CrystalRuby.log_error e.backtrace
78
+ end
79
+ end
80
+
81
+ def thread_id
82
+ Thread.current.object_id
83
+ end
84
+
85
+ def yield!(lib: nil, time: 0.0)
86
+ schedule_work!(lib, :yield, nil, async: false, blocking: false, lib: lib) if running? && lib
87
+ nil
88
+ end
89
+
90
+ def current_thread_id=(val)
91
+ @current_thread_id = val
92
+ end
93
+
94
+ def current_thread_id
95
+ @current_thread_id
96
+ end
97
+
98
+ def schedule_work!(receiver, op_name, *args, return_type, blocking: true, async: true, lib: nil)
99
+ if @single_thread_mode
100
+ unless Thread.current.object_id == @main_thread_id
101
+ raise SingleThreadViolation,
102
+ "Single thread mode is enabled, cannot run in multi-threaded mode. " \
103
+ "Reactor was started from: #{@main_thread_id}, then called from #{Thread.current.object_id}"
104
+ end
105
+
106
+ return receiver.send(op_name, *args)
107
+ end
108
+
109
+ tvars = thread_conditions
110
+ tvars[:mux].synchronize do
111
+ REACTOR_QUEUE.push(
112
+ case true
113
+ when async
114
+ lambda {
115
+ receiver.send(
116
+ op_name, *args, tvars[:thread_id],
117
+ CALLBACKS_MAP[return_type]
118
+ )
119
+ yield!(lib: lib, time: 0)
120
+ }
121
+ when blocking
122
+ lambda {
123
+ tvars[:error] = nil
124
+ Reactor.current_thread_id = tvars[:thread_id]
125
+ begin
126
+ result = receiver.send(op_name, *args)
127
+ rescue StandardError => e
128
+ tvars[:error] = e
129
+ end
130
+ tvars[:result] = result unless tvars[:error]
131
+ tvars[:cond].signal
132
+ }
133
+ else
134
+ lambda {
135
+ outstanding_jobs = receiver.send(op_name, *args)
136
+ yield!(lib: lib, time: 0) unless outstanding_jobs == 0
137
+ }
138
+ end
139
+ )
140
+ return await_result! if blocking
141
+ end
142
+ end
143
+
144
+ def running?
145
+ @main_loop&.alive?
146
+ end
147
+
148
+ def init_single_thread_mode!
149
+ @single_thread_mode ||= begin
150
+ @main_thread_id = Thread.current.object_id
151
+ true
152
+ end
153
+ end
154
+ end
155
+ end
@@ -2,11 +2,11 @@ module CrystalRuby
2
2
  module Template
3
3
  Dir[File.join(File.dirname(__FILE__), "templates", "*.cr")].each do |file|
4
4
  template_name = File.basename(file, File.extname(file)).split("_").map(&:capitalize).join
5
- const_set(template_name, File.read(file))
6
- end
7
-
8
- def self.render(template, context)
9
- template % context
5
+ template_value = File.read(file)
6
+ template_value.define_singleton_method(:render) do |context|
7
+ self % context
8
+ end
9
+ const_set(template_name, template_value)
10
10
  end
11
11
  end
12
12
  end
@@ -12,7 +12,6 @@ end
12
12
  # This function is the entry point for the CrystalRuby code, exposed through FFI.
13
13
  # We apply some basic error handling here, and convert the arguments and return values
14
14
  # to ensure that we are using Crystal native types.
15
-
16
15
  fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type}
17
16
  begin
18
17
  %{convert_lib_args}
@@ -20,10 +19,44 @@ fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type}
20
19
  return_value = %{module_name}.%{fn_name}(%{arg_names})
21
20
  return %{convert_return_type}
22
21
  rescue ex
23
- CrystalRuby.report_error("RuntimeError", ex.message.to_s)
22
+ CrystalRuby.report_error("RuntimeError", ex.message.to_s, 0)
24
23
  end
25
24
  rescue ex
26
- CrystalRuby.report_error("ArgumentError", ex.message.to_s)
25
+ CrystalRuby.report_error("ArgumentError", ex.message.to_s, 0)
27
26
  end
28
27
  return %{error_value}
29
28
  end
29
+
30
+
31
+ # This function is the async entry point for the CrystalRuby code, exposed through FFI.
32
+ # We apply some basic error handling here, and convert the arguments and return values
33
+ # to ensure that we are using Crystal native types.
34
+ fun %{lib_fn_name}_async(%{lib_fn_args} thread_id: UInt32, callback : %{callback_type}): Void
35
+ begin
36
+ %{convert_lib_args}
37
+ CrystalRuby.increment_task_counter
38
+ spawn do
39
+ begin
40
+ return_value = %{module_name}.%{fn_name}(%{arg_names})
41
+ converted = %{convert_return_type}
42
+ CrystalRuby.queue_callback(->{
43
+ %{callback_call}
44
+ CrystalRuby.decrement_task_counter
45
+ })
46
+ rescue ex
47
+ exception = ex.message.to_s
48
+ CrystalRuby.queue_callback(->{
49
+ CrystalRuby.error_callback.call("RuntimeError".to_unsafe, exception.to_unsafe, thread_id)
50
+ CrystalRuby.decrement_task_counter
51
+ })
52
+ end
53
+ end
54
+ rescue ex
55
+
56
+ exception = ex.message.to_s
57
+ CrystalRuby.queue_callback(->{
58
+ CrystalRuby.error_callback.call("RuntimeError".to_unsafe, ex.message.to_s.to_unsafe, thread_id)
59
+ CrystalRuby.decrement_task_counter
60
+ })
61
+ end
62
+ end