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.
- checksums.yaml +4 -4
- data/.dockerignore +10 -0
- data/README.md +109 -25
- data/exe/crystalruby +14 -14
- data/lib/crystalruby/adapter.rb +133 -0
- data/lib/crystalruby/compilation.rb +16 -63
- data/lib/crystalruby/config.rb +27 -11
- data/lib/crystalruby/function.rb +228 -0
- data/lib/crystalruby/library.rb +198 -0
- data/lib/crystalruby/reactor.rb +155 -0
- data/lib/crystalruby/template.rb +5 -5
- data/lib/crystalruby/templates/function.cr +36 -3
- data/lib/crystalruby/templates/index.cr +112 -17
- data/lib/crystalruby/typebuilder.rb +2 -0
- data/lib/crystalruby/typemaps.rb +95 -4
- data/lib/crystalruby/types/type_serializer/json.rb +3 -2
- data/lib/crystalruby/version.rb +1 -1
- data/lib/crystalruby.rb +26 -333
- data/lib/module.rb +1 -1
- metadata +7 -2
@@ -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
|
data/lib/crystalruby/template.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|