crystalruby 0.1.13 → 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 +37 -17
- data/exe/crystalruby +12 -14
- data/lib/crystalruby/adapter.rb +98 -70
- data/lib/crystalruby/compilation.rb +13 -82
- data/lib/crystalruby/config.rb +14 -17
- data/lib/crystalruby/function.rb +228 -0
- data/lib/crystalruby/library.rb +198 -0
- data/lib/crystalruby/reactor.rb +32 -61
- data/lib/crystalruby/template.rb +0 -1
- data/lib/crystalruby/templates/index.cr +33 -12
- data/lib/crystalruby/typebuilder.rb +2 -0
- data/lib/crystalruby/typemaps.rb +87 -1
- data/lib/crystalruby/types/type_serializer/json.rb +3 -2
- data/lib/crystalruby/version.rb +1 -1
- data/lib/crystalruby.rb +24 -253
- metadata +5 -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
|
data/lib/crystalruby/reactor.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
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
|
+
|
2
9
|
module Reactor
|
3
10
|
module_function
|
4
11
|
|
5
|
-
class ReactorStoppedException < StandardError; end
|
6
12
|
class SingleThreadViolation < StandardError; end
|
7
13
|
|
8
14
|
REACTOR_QUEUE = Queue.new
|
@@ -40,6 +46,8 @@ module CrystalRuby
|
|
40
46
|
is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception)
|
41
47
|
error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
|
42
48
|
tid = tid.zero? ? Reactor.current_thread_id : tid
|
49
|
+
raise error_type.new(message) unless THREAD_MAP.key?(tid)
|
50
|
+
|
43
51
|
THREAD_MAP[tid][:error] = error_type.new(message)
|
44
52
|
THREAD_MAP[tid][:result] = nil
|
45
53
|
THREAD_MAP[tid][:cond].signal
|
@@ -57,15 +65,26 @@ module CrystalRuby
|
|
57
65
|
THREAD_MAP[thread_id][:result]
|
58
66
|
end
|
59
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
|
+
|
60
81
|
def thread_id
|
61
82
|
Thread.current.object_id
|
62
83
|
end
|
63
84
|
|
64
|
-
def yield!(time: 0)
|
65
|
-
|
66
|
-
|
67
|
-
schedule_work!(Reactor, :yield, nil, async: false, blocking: false)
|
68
|
-
end
|
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
|
69
88
|
end
|
70
89
|
|
71
90
|
def current_thread_id=(val)
|
@@ -76,9 +95,7 @@ module CrystalRuby
|
|
76
95
|
@current_thread_id
|
77
96
|
end
|
78
97
|
|
79
|
-
def schedule_work!(receiver, op_name, *args, return_type, blocking: true, async: true)
|
80
|
-
raise ReactorStoppedException, "Reactor has been terminated, no new work can be scheduled" if @stopped
|
81
|
-
|
98
|
+
def schedule_work!(receiver, op_name, *args, return_type, blocking: true, async: true, lib: nil)
|
82
99
|
if @single_thread_mode
|
83
100
|
unless Thread.current.object_id == @main_thread_id
|
84
101
|
raise SingleThreadViolation,
|
@@ -99,7 +116,7 @@ module CrystalRuby
|
|
99
116
|
op_name, *args, tvars[:thread_id],
|
100
117
|
CALLBACKS_MAP[return_type]
|
101
118
|
)
|
102
|
-
yield!(time: 0)
|
119
|
+
yield!(lib: lib, time: 0)
|
103
120
|
}
|
104
121
|
when blocking
|
105
122
|
lambda {
|
@@ -116,7 +133,7 @@ module CrystalRuby
|
|
116
133
|
else
|
117
134
|
lambda {
|
118
135
|
outstanding_jobs = receiver.send(op_name, *args)
|
119
|
-
yield!(time: 0
|
136
|
+
yield!(lib: lib, time: 0) unless outstanding_jobs == 0
|
120
137
|
}
|
121
138
|
end
|
122
139
|
)
|
@@ -124,61 +141,15 @@ module CrystalRuby
|
|
124
141
|
end
|
125
142
|
end
|
126
143
|
|
127
|
-
def init_single_thread_mode!
|
128
|
-
@single_thread_mode = true
|
129
|
-
@main_thread_id = Thread.current.object_id
|
130
|
-
init_crystal_ruby!
|
131
|
-
end
|
132
|
-
|
133
|
-
def init_crystal_ruby!
|
134
|
-
attach_lib!
|
135
|
-
init(ERROR_CALLBACK)
|
136
|
-
end
|
137
|
-
|
138
|
-
def attach_lib!
|
139
|
-
CrystalRuby.log_debug("Attaching lib")
|
140
|
-
extend FFI::Library
|
141
|
-
ffi_lib CrystalRuby.config.crystal_lib_dir / CrystalRuby.config.crystal_lib_name
|
142
|
-
attach_function :init, [:pointer], :void
|
143
|
-
attach_function :stop, [], :void
|
144
|
-
attach_function :yield, %i[], :int
|
145
|
-
end
|
146
|
-
|
147
|
-
def stop!
|
148
|
-
CrystalRuby.log_debug("Stopping reactor")
|
149
|
-
@stopped = true
|
150
|
-
sleep 1
|
151
|
-
@main_loop&.kill
|
152
|
-
@main_loop = nil
|
153
|
-
CrystalRuby.log_debug("Reactor stopped")
|
154
|
-
end
|
155
|
-
|
156
144
|
def running?
|
157
145
|
@main_loop&.alive?
|
158
146
|
end
|
159
147
|
|
160
|
-
def
|
161
|
-
@
|
162
|
-
|
163
|
-
|
164
|
-
CrystalRuby.log_debug("Starting reactor")
|
165
|
-
init(ERROR_CALLBACK)
|
166
|
-
CrystalRuby.log_debug("CrystalRuby initialized")
|
167
|
-
loop do
|
168
|
-
REACTOR_QUEUE.pop[]
|
169
|
-
break if @stopped
|
170
|
-
end
|
171
|
-
stop
|
172
|
-
CrystalRuby.log_debug("Stopping reactor")
|
173
|
-
rescue StandardError => e
|
174
|
-
puts "Error: #{e}"
|
175
|
-
puts e.backtrace
|
176
|
-
end
|
148
|
+
def init_single_thread_mode!
|
149
|
+
@single_thread_mode ||= begin
|
150
|
+
@main_thread_id = Thread.current.object_id
|
151
|
+
true
|
177
152
|
end
|
178
153
|
end
|
179
|
-
|
180
|
-
at_exit do
|
181
|
-
@stopped = true
|
182
|
-
end
|
183
154
|
end
|
184
155
|
end
|
data/lib/crystalruby/template.rb
CHANGED
@@ -4,7 +4,6 @@ module CrystalRuby
|
|
4
4
|
template_name = File.basename(file, File.extname(file)).split("_").map(&:capitalize).join
|
5
5
|
template_value = File.read(file)
|
6
6
|
template_value.define_singleton_method(:render) do |context|
|
7
|
-
CrystalRuby.log_debug("Template.render: #{template_name}")
|
8
7
|
self % context
|
9
8
|
end
|
10
9
|
const_set(template_name, template_value)
|
@@ -1,13 +1,15 @@
|
|
1
|
-
|
1
|
+
module CrystalRuby
|
2
2
|
|
3
|
-
ARGV1 = "crystalruby"
|
4
|
-
CALLBACK_MUX = Mutex.new
|
3
|
+
ARGV1 = "crystalruby"
|
4
|
+
CALLBACK_MUX = Mutex.new
|
5
|
+
|
6
|
+
alias ErrorCallback = (Pointer(UInt8), Pointer(UInt8), UInt32 -> Void)
|
5
7
|
|
6
|
-
module CrystalRuby
|
7
8
|
# Initializing Crystal Ruby invokes init on the Crystal garbage collector.
|
8
9
|
# We need to be sure to only do this once.
|
9
10
|
@@initialized = false
|
10
11
|
|
12
|
+
@@libname = "crystalruby"
|
11
13
|
# Our Ruby <-> Crystal Reactor uses Fibers, with callbacks to allow
|
12
14
|
# multiple concurrent Crystal operations to be queued
|
13
15
|
@@callbacks = [] of Proc(Nil)
|
@@ -25,13 +27,13 @@ module CrystalRuby
|
|
25
27
|
# 1. Initialize the Crystal garbage collector
|
26
28
|
# 2. Set the error callback
|
27
29
|
# 3. Call the Crystal main function
|
28
|
-
def self.init(error_callback : ErrorCallback)
|
30
|
+
def self.init(libname : Pointer(UInt8), error_callback : ErrorCallback)
|
29
31
|
return if @@initialized
|
30
32
|
@@initialized = true
|
31
|
-
GC.init
|
32
33
|
argv_ptr = ARGV1.to_unsafe
|
33
|
-
Crystal.
|
34
|
+
Crystal.main_user_code(0, pointerof(argv_ptr))
|
34
35
|
@@error_callback = error_callback
|
36
|
+
@@libname = String.new(libname)
|
35
37
|
end
|
36
38
|
|
37
39
|
# Explicit error handling (triggers exception within Ruby on the same thread)
|
@@ -70,6 +72,10 @@ module CrystalRuby
|
|
70
72
|
@@callbacks.size
|
71
73
|
end
|
72
74
|
|
75
|
+
def self.libname : String
|
76
|
+
@@libname
|
77
|
+
end
|
78
|
+
|
73
79
|
# Flush all callbacks
|
74
80
|
def self.flush_callbacks : Int32
|
75
81
|
CALLBACK_MUX.synchronize do
|
@@ -84,12 +90,28 @@ module CrystalRuby
|
|
84
90
|
end
|
85
91
|
|
86
92
|
# Initialize CrystalRuby
|
87
|
-
fun init(cb : ErrorCallback): Void
|
88
|
-
CrystalRuby.init(cb)
|
93
|
+
fun init(libname : Pointer(UInt8), cb : CrystalRuby::ErrorCallback): Void
|
94
|
+
CrystalRuby.init(libname, cb)
|
95
|
+
end
|
96
|
+
|
97
|
+
fun stop : Void
|
98
|
+
LibGC.deinit()
|
89
99
|
end
|
90
100
|
|
91
|
-
|
92
|
-
|
101
|
+
@[Link("gc")]
|
102
|
+
lib LibGC
|
103
|
+
$stackbottom = GC_stackbottom : Void*
|
104
|
+
fun deinit = GC_deinit
|
105
|
+
end
|
106
|
+
|
107
|
+
module GC
|
108
|
+
def self.current_thread_stack_bottom
|
109
|
+
{Pointer(Void).null, LibGC.stackbottom}
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.set_stackbottom(stack_bottom : Void*)
|
113
|
+
LibGC.stackbottom = stack_bottom
|
114
|
+
end
|
93
115
|
end
|
94
116
|
|
95
117
|
# Yield to the Crystal scheduler from Ruby
|
@@ -99,7 +121,6 @@ end
|
|
99
121
|
# once this figure reaches 0).
|
100
122
|
fun yield() : Int32
|
101
123
|
if CrystalRuby.count_callbacks == 0
|
102
|
-
|
103
124
|
Fiber.yield
|
104
125
|
|
105
126
|
# TODO: We should apply backpressure here to prevent busy waiting if the number of outstanding tasks is not decreasing.
|
@@ -19,6 +19,7 @@ module CrystalRuby
|
|
19
19
|
nil
|
20
20
|
end
|
21
21
|
restores << [context, method_name, old_method]
|
22
|
+
context.singleton_class.undef_method(method_name) if old_method
|
22
23
|
context.define_singleton_method(method_name) do |*args|
|
23
24
|
Types.send(method_name, *args)
|
24
25
|
end
|
@@ -26,6 +27,7 @@ module CrystalRuby
|
|
26
27
|
yield
|
27
28
|
ensure
|
28
29
|
restores.each do |context, method_name, old_method|
|
30
|
+
context.singleton_class.undef_method(method_name)
|
29
31
|
context.define_singleton_method(method_name, old_method) if old_method
|
30
32
|
end
|
31
33
|
end
|