crystalruby 0.1.12 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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