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