crystalruby 0.1.13 → 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 +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
|