crystalruby 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 542a695dee21acb928d1c758ce8a48f31422b8b71940b6ea61d87c2744b3fc5c
4
- data.tar.gz: 988da32111ca2a2a13f1b8da93c850a76ea6dda4191f55feabd768b35444e316
3
+ metadata.gz: 0c9b23b060f8c69dea9daa64e7842bc795b9b6039177dd5b1f9dac2067f5cefe
4
+ data.tar.gz: 1d6bdf9475bca49e8298e58378ef7391a6f7acee3e68c310d6d4f41b0090e69f
5
5
  SHA512:
6
- metadata.gz: c5109478ed499c9737dc008cae0f720ae03d30a31bffb9add0034303ba7d39b943b437074fb033b52f307cb09f838d64a1a037b36f80bf985e4141614b8ec983
7
- data.tar.gz: 4e1f0831bf0eb15a889ad7ca0bd797ae02377968f41b365593102663ff828b0791cd500871e6ba3145b4613a2ed797690ca0b4866e57ee18d9032ddee546aa96
6
+ metadata.gz: c63ab3d286e099b28ca64d24d3500132959056745b3eb47ce92bca0b2d6615b2027a590f1c3844e789fffdf610bf06babb07842eedc22a66d55d7e7e01060cd8
7
+ data.tar.gz: 50ff5f69decbfe21785d2f6951c576a7efbbab51ff02660e2cc4ea019122ae94b28459fb7572e1702155feb8e6669b1013779dd2c41461024a65ce45f45e341e
data/crystalruby.gemspec CHANGED
@@ -34,8 +34,8 @@ Gem::Specification.new do |spec|
34
34
  # spec.add_dependency "example-gem", "~> 1.0"
35
35
  spec.add_dependency "digest"
36
36
  spec.add_dependency "ffi"
37
- spec.add_dependency "fileutils"
38
- spec.add_dependency "prism"
37
+ spec.add_dependency "fileutils", "~> 1.7"
38
+ spec.add_dependency "prism", "~> 1.3.0"
39
39
  # For more information and examples about making a new gem, check out our
40
40
  # guide at: https://bundler.io/guides/creating_gem.html
41
41
  end
@@ -36,7 +36,7 @@ module CrystalRuby
36
36
  # @option options [Boolean] :async (false) Mark the method as async (allows multiplexing).
37
37
  # @option options [String] :lib ("crystalruby") The name of the library to compile the Crystal code into.
38
38
  # @option options [Proc] :block An optional wrapper Ruby block that wraps around any invocations of the crystal code
39
- def crystallize( returns=:void, raw: false, async: false, lib: "crystalruby", &block)
39
+ def crystallize(returns = :void, raw: false, async: false, lib: "crystalruby", &block)
40
40
  (self == TOPLEVEL_BINDING.receiver ? Object : self).instance_eval do
41
41
  @crystallize_next = {
42
42
  raw: raw,
@@ -49,7 +49,7 @@ module CrystalRuby
49
49
  end
50
50
 
51
51
  # Alias for `crystallize`
52
- alias :crystalize :crystallize
52
+ alias crystalize crystallize
53
53
 
54
54
  # Exposes a Ruby method to one or more Crystal libraries.
55
55
  # Type annotations follow the same rules as the `crystallize` method, but are
@@ -58,7 +58,7 @@ module CrystalRuby
58
58
  # @param [Hash] options The options hash.
59
59
  # @option options [Boolean] :raw (false) Pass raw Crystal code to the compiler as a string.
60
60
  # @option options [String] :libs (["crystalruby"]) The name of the Crystal librarie(s) to expose the Ruby code to.
61
- def expose_to_crystal( returns=:void, libs: ["crystalruby"])
61
+ def expose_to_crystal(returns = :void, libs: ["crystalruby"])
62
62
  (self == TOPLEVEL_BINDING.receiver ? Object : self).instance_eval do
63
63
  @expose_next_to_crystal = {
64
64
  returns: returns,
@@ -70,7 +70,7 @@ module CrystalRuby
70
70
  # Define a shard dependency
71
71
  # This dependency will be automatically injected into the shard.yml file for
72
72
  # the given library and installed upon compile if it is not already installed.
73
- def shard(shard_name, lib: 'crystalruby', **opts)
73
+ def shard(shard_name, lib: "crystalruby", **opts)
74
74
  CrystalRuby::Library[lib].require_shard(shard_name, **opts)
75
75
  end
76
76
 
@@ -78,14 +78,18 @@ module CrystalRuby
78
78
  # This is useful for defining classes, modules, performing set-up tasks etc.
79
79
  # See: docs for .crystallize to understand the `raw` and `lib` parameters.
80
80
  def crystal(raw: false, lib: "crystalruby", &block)
81
- inline_crystal_body = respond_to?(:name) ? Template::InlineChunk.render(
82
- {
83
- module_name: name,
84
- body: SourceReader.extract_source_from_proc(block, raw: raw),
85
- mod_or_class: self.kind_of?(Class) && self < Types::Type ? "class" : "module",
86
- superclass: self.kind_of?(Class) && self < Types::Type ? "< #{self.crystal_supertype}" : ""
87
- }) :
88
- SourceReader.extract_source_from_proc(block, raw: raw)
81
+ inline_crystal_body = if respond_to?(:name)
82
+ Template::InlineChunk.render(
83
+ {
84
+ module_name: name,
85
+ body: SourceReader.extract_source_from_proc(block, raw: raw),
86
+ mod_or_class: is_a?(Class) && self < Types::Type ? "class" : "module",
87
+ superclass: is_a?(Class) && self < Types::Type ? "< #{crystal_supertype}" : ""
88
+ }
89
+ )
90
+ else
91
+ SourceReader.extract_source_from_proc(block, raw: raw)
92
+ end
89
93
 
90
94
  CrystalRuby::Library[lib].crystallize_chunk(
91
95
  self,
@@ -94,7 +98,6 @@ module CrystalRuby
94
98
  )
95
99
  end
96
100
 
97
-
98
101
  # This method provides a useful DSL for defining Crystal types in pure Ruby
99
102
  # MyType = CRType{ Int32 | Hash(String, Array(Bool) | Float65 | Nil) }
100
103
  # @param [Proc] block The block within which we build the type definition.
@@ -152,14 +155,16 @@ module CrystalRuby
152
155
 
153
156
  owner = method.owner.singleton_class? ? method.owner.attached_object : method.owner
154
157
  owner.class_eval(src)
155
- owner.instance_eval(src) unless method.kind_of?(UnboundMethod) && method.owner.ancestors.include?(CrystalRuby::Types::Type)
156
- method = owner.send(method.kind_of?(UnboundMethod) ? :instance_method : :method, method.name)
158
+ unless method.is_a?(UnboundMethod) && method.owner.ancestors.include?(CrystalRuby::Types::Type)
159
+ owner.instance_eval(src)
160
+ end
161
+ method = owner.send(method.is_a?(UnboundMethod) ? :instance_method : :method, method.name)
157
162
 
158
163
  libs.each do |lib|
159
164
  CrystalRuby::Library[lib].expose_method(
160
165
  method,
161
166
  args,
162
- returns,
167
+ returns
163
168
  )
164
169
  end
165
170
  end
@@ -66,6 +66,7 @@ module CrystalRuby
66
66
  raise ArgumentError, "no block given but function expects block" if !blk && func.takes_block?
67
67
 
68
68
  args << blk if blk
69
+
69
70
  func.map_args!(args)
70
71
  args.unshift(memory) if func.instance_method
71
72
 
@@ -86,27 +87,35 @@ module CrystalRuby
86
87
  def register_callback!
87
88
  return unless ruby
88
89
 
89
- @callback_func = FFI::Function.new(ffi_ret_type, ffi_types) do |*args|
90
+ ret_type = ffi_ret_type == :string ? :pointer : ffi_ret_type
91
+ @callback_func = FFI::Function.new(ret_type, ffi_types) do |*args|
90
92
  receiver = instance_method ? owner.new(args.shift) : owner
91
- ret_val = if takes_block?
92
- block_arg = arg_type_map[:__yield_to][:crystalruby_type].new(args.pop)
93
- receiver.send(name, *unmap_args(args)) do |*args|
94
- args = args.map.with_index do |arg, i|
95
- arg = block_arg.inner_types[i].new(arg) unless arg.is_a?(block_arg.inner_types[i])
96
- arg.memory
97
- end
98
- return_val = block_arg.invoke(*args)
99
- unless return_val.is_a?(block_arg.inner_types[-1])
100
- return_val = block_arg.inner_types[-1].new(return_val)
101
- end
102
- block_arg.inner_types[-1].anonymous? ? return_val.value : return_val
103
- end
104
- else
105
- receiver.send(name, *unmap_args(args))
106
- end
93
+ ret_val = \
94
+ if takes_block?
95
+ block_arg = arg_type_map[:__yield_to][:crystalruby_type].new(args.pop)
96
+ receiver.send(name, *unmap_args(args)) do |*args|
97
+ args = args.map.with_index do |arg, i|
98
+ arg = block_arg.inner_types[i].new(arg) unless arg.is_a?(block_arg.inner_types[i])
99
+ arg.memory
100
+ end
101
+ return_val = block_arg.invoke(*args)
102
+ return_val = block_arg.inner_types[-1].new(return_val) unless return_val.is_a?(block_arg.inner_types[-1])
103
+ block_arg.inner_types[-1].anonymous? ? return_val.value : return_val
104
+ end
105
+ else
106
+ receiver.send(name, *unmap_args(args))
107
+ end
107
108
  unmap_retval(ret_val)
108
109
  end
109
- Reactor.schedule_work!(lib, :"register_#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback", @callback_func, :void, blocking: true, async: false)
110
+
111
+ Reactor.schedule_work!(
112
+ lib,
113
+ :"register_#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback",
114
+ @callback_func,
115
+ :void,
116
+ blocking: true,
117
+ async: false
118
+ )
110
119
  end
111
120
 
112
121
  # Attaches the crystallized FFI functions to their related Ruby modules and classes.
@@ -245,7 +254,7 @@ module CrystalRuby
245
254
 
246
255
  def register_custom_types!(lib)
247
256
  custom_types.each do |crystalruby_type|
248
- next unless crystalruby_type.is_a?(Class) && crystalruby_type < Types::Type
257
+ next unless Types::Type.subclass?(crystalruby_type)
249
258
 
250
259
  [*crystalruby_type.nested_types].uniq.each do |type|
251
260
  lib.register_type!(type)
@@ -263,7 +272,7 @@ module CrystalRuby
263
272
 
264
273
  mapped = argmap[args[index]]
265
274
  case mapped
266
- when CrystalRuby::Types::Type then
275
+ when CrystalRuby::Types::Type
267
276
  args[index] = mapped.memory
268
277
  (refs ||= []) << mapped
269
278
  else
@@ -291,10 +300,12 @@ module CrystalRuby
291
300
  end
292
301
 
293
302
  def unmap_retval(retval)
303
+ return FFI::MemoryPointer.from_string(retval) if return_type_map[:ffi_ret_type] == :string
294
304
  return retval unless return_type_map[:arg_mapper]
295
305
 
296
306
  retval = return_type_map[:arg_mapper][retval]
297
- retval = retval.memory if retval.kind_of?(CrystalRuby::Types::Type)
307
+
308
+ retval = retval.memory if retval.is_a?(CrystalRuby::Types::Type)
298
309
  retval
299
310
  end
300
311
 
@@ -214,9 +214,7 @@ module CrystalRuby
214
214
  unless compiled?
215
215
  FileUtils.rm_f(lib_file)
216
216
 
217
- if shard_dependencies.any? && shards.empty?
218
- rewrite_shards_file!
219
- end
217
+ rewrite_shards_file! if shard_dependencies.any? && shards.empty?
220
218
 
221
219
  CrystalRuby::Compilation.install_shards!(src_dir)
222
220
  CrystalRuby::Compilation.compile!(
@@ -239,14 +237,16 @@ module CrystalRuby
239
237
  singleton_class.class_eval do
240
238
  extend FFI::Library
241
239
  ffi_lib lib_file
242
- %i[yield init].each do |method_name|
240
+ %i[yield init gc].each do |method_name|
243
241
  singleton_class.undef_method(method_name) if singleton_class.method_defined?(method_name)
244
242
  undef_method(method_name) if method_defined?(method_name)
245
243
  end
246
244
  attach_function :init, %i[string pointer pointer], :void
247
245
  attach_function :yield, %i[], :int
246
+ attach_function :gc, %i[], :void
248
247
  lib_methods.each_value.select(&:ruby).each do |method|
249
- attach_function :"register_#{method.name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback", %i[pointer], :void
248
+ attach_function :"register_#{method.name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback",
249
+ %i[pointer], :void
250
250
  end
251
251
  end
252
252
 
@@ -1,5 +1,6 @@
1
+ require "json"
2
+
1
3
  module CrystalRuby
2
- require 'json'
3
4
  # The Reactor represents a singleton Thread responsible for running all Ruby/crystal interop code.
4
5
  # Crystal's Fiber scheduler and GC assume all code is run on a single thread.
5
6
  # This class is responsible for multiplexing Ruby and Crystal code onto a single thread.
@@ -15,6 +16,13 @@ module CrystalRuby
15
16
 
16
17
  REACTOR_QUEUE = Queue.new
17
18
 
19
+ # Invoke GC every 100 ops
20
+ GC_OP_THRESHOLD = ENV.fetch("CRYSTAL_GC_OP_THRESHOLD", 100).to_i
21
+ # Or every 0.05 seconds
22
+ GC_INTERVAL = ENV.fetch("CRYSTAL_GC_INTERVAL", 0.05).to_f
23
+ # Or if we've gotten hold of a reference to at least 100KB or more of fresh memory since last GC
24
+ GC_BYTES_SEEN_THRESHOLD = ENV.fetch("CRYSTAL_GC_BYTES_SEEN_THRESHOLD", 100 * 1024).to_i
25
+
18
26
  # We maintain a map of threads, each with a mutex, condition variable, and result
19
27
  THREAD_MAP = Hash.new do |h, tid_or_thread, tid = tid_or_thread|
20
28
  if tid_or_thread.is_a?(Thread)
@@ -49,7 +57,7 @@ module CrystalRuby
49
57
  error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
50
58
  error = error_type.new(message)
51
59
  error.set_backtrace(JSON.parse(backtrace))
52
- raise error unless THREAD_MAP.key?(tid)
60
+ raise error unless THREAD_MAP.key?(tid)
53
61
 
54
62
  THREAD_MAP[tid][:error] = error
55
63
  THREAD_MAP[tid][:result] = nil
@@ -62,10 +70,12 @@ module CrystalRuby
62
70
 
63
71
  def await_result!
64
72
  mux, cond, result, err = thread_conditions.values_at(:mux, :cond, :result, :error)
65
- cond.wait(mux) unless (result || err)
73
+ cond.wait(mux) unless result || err
66
74
  result, err, thread_conditions[:result], thread_conditions[:error] = thread_conditions.values_at(:result, :error)
67
75
  if err
68
- combined_backtrace = err.backtrace[0..(err.backtrace.index{|m| m.include?('call_blocking_function')} || 2) - 3] + caller[5..-1]
76
+ combined_backtrace = err.backtrace[0..(err.backtrace.index { |m|
77
+ m.include?("call_blocking_function")
78
+ } || 2) - 3] + caller[5..-1]
69
79
  err.set_backtrace(combined_backtrace)
70
80
  raise err
71
81
  end
@@ -78,22 +88,25 @@ module CrystalRuby
78
88
  end
79
89
 
80
90
  def stop!
81
- if @main_loop
82
- schedule_work!(self, :halt_loop!, :void, blocking: true, async: false)
83
- @main_loop.join
84
- @main_loop = nil
85
- CrystalRuby.log_info "Reactor loop stopped"
86
- end
91
+ return unless @main_loop
92
+
93
+ schedule_work!(self, :halt_loop!, :void, blocking: true, async: false)
94
+ @main_loop.join
95
+ @main_loop = nil
96
+ CrystalRuby.log_info "Reactor loop stopped"
87
97
  end
88
98
 
89
99
  def start!
100
+ @op_count = 0
90
101
  @main_loop ||= Thread.new do
91
102
  @main_thread_id = Thread.current.object_id
92
103
  CrystalRuby.log_debug("Starting reactor")
93
104
  CrystalRuby.log_debug("CrystalRuby initialized")
94
105
  while true
95
- handler, *args = REACTOR_QUEUE.pop
96
- send(handler, *args)
106
+ handler, *args, lib = REACTOR_QUEUE.pop
107
+ send(handler, *args, lib)
108
+ @op_count += 1
109
+ invoke_gc_if_due!(lib)
97
110
  end
98
111
  rescue StopReactor => e
99
112
  rescue StandardError => e
@@ -102,6 +115,46 @@ module CrystalRuby
102
115
  end
103
116
  end
104
117
 
118
+ def invoke_gc_if_due!(lib)
119
+ schedule_work!(lib, :gc, :void, blocking: true, async: false, lib: lib) if lib && gc_due?
120
+ end
121
+
122
+ def gc_due?
123
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
124
+
125
+ # Initialize state variables if not already set.
126
+ @last_gc_time ||= now
127
+ @last_gc_op_count ||= @op_count
128
+ @last_mem_check_time ||= now
129
+
130
+ # Calculate differences based on ops and time.
131
+ ops_since_last_gc = @op_count - @last_gc_op_count
132
+ time_since_last_gc = now - @last_gc_time
133
+
134
+ # Start with our two “cheap” conditions.
135
+ due = (ops_since_last_gc >= GC_OP_THRESHOLD) || (time_since_last_gc >= GC_INTERVAL) || Types::Allocator.gc_bytes_seen > GC_BYTES_SEEN_THRESHOLD
136
+
137
+ if due
138
+ # Update the baseline values after GC is scheduled.
139
+ @last_gc_time = now
140
+ # If we just did a memory check, use that value; otherwise, fetch one now.
141
+ @last_gc_op_count = @op_count
142
+ Types::Allocator.gc_hint_reset!
143
+ true
144
+ else
145
+ false
146
+ end
147
+ end
148
+
149
+ def start_gc_thread!(lib)
150
+ Thread.new do
151
+ loop do
152
+ schedule_work!(lib, :gc, :void, blocking: true, async: false, lib: lib) if gc_due?
153
+ sleep GC_INTERVAL
154
+ end
155
+ end
156
+ end
157
+
105
158
  def thread_id
106
159
  Thread.current.object_id
107
160
  end
@@ -116,7 +169,7 @@ module CrystalRuby
116
169
  yield!(lib: lib, time: 0)
117
170
  end
118
171
 
119
- def invoke_blocking!(receiver, op_name, *args, tvars)
172
+ def invoke_blocking!(receiver, op_name, *args, tvars, _lib)
120
173
  tvars[:error] = nil
121
174
  begin
122
175
  tvars[:result] = receiver.send(op_name, *args)
@@ -141,6 +194,7 @@ module CrystalRuby
141
194
  "Single thread mode is enabled, cannot run in multi-threaded mode. " \
142
195
  "Reactor was started from: #{@main_thread_id}, then called from #{Thread.current.object_id}"
143
196
  end
197
+ invoke_gc_if_due!(lib)
144
198
  return receiver.send(op_name, *args)
145
199
  end
146
200
 
@@ -149,7 +203,7 @@ module CrystalRuby
149
203
  REACTOR_QUEUE.push(
150
204
  case true
151
205
  when async then [:invoke_async!, receiver, op_name, *args, tvars[:thread_id], CALLBACKS_MAP[return_type], lib]
152
- when blocking then [:invoke_blocking!, receiver, op_name, *args, tvars]
206
+ when blocking then [:invoke_blocking!, receiver, op_name, *args, tvars, lib]
153
207
  else [:invoke_await!, receiver, op_name, *args, lib]
154
208
  end
155
209
  )
@@ -4,12 +4,12 @@ module CrystalRuby
4
4
 
5
5
  # Reads code line by line from a given source location and returns the first valid Ruby expression found
6
6
  def extract_expr_from_source_location(source_location)
7
- lines = source_location.then{|f,l| IO.readlines(f)[l-1..]}
7
+ lines = source_location.then { |f, l| IO.readlines(f)[l - 1..] }
8
8
  lines[0] = lines[0][/CRType.*/] if lines[0] =~ /<\s+CRType/ || lines[0] =~ /= CRType/
9
9
  lines.each.with_object([]) do |line, expr_source|
10
- break expr_source.join("") if (Prism.parse((expr_source << line).join("")).success?)
10
+ break expr_source.join("") if Prism.parse((expr_source << line).join("")).success?
11
11
  end
12
- rescue
12
+ rescue StandardError
13
13
  raise "Failed to extract expression from source location: #{source_location}. Ensure the file exists and the line number is correct. Extraction from a REPL is not supported"
14
14
  end
15
15
 
@@ -27,26 +27,25 @@ module CrystalRuby
27
27
  block_source = extract_expr_from_source_location(block.source_location)
28
28
  parsed_source = Prism.parse(block_source).value
29
29
 
30
- node = parsed_source.statements.body[0].arguments&.arguments&.find{|x| search_node(x, Prism::StatementsNode) }
30
+ node = parsed_source.statements.body[0].arguments&.arguments&.find { |x| search_node(x, Prism::StatementsNode) }
31
31
  node ||= parsed_source.statements.body[0]
32
- body_node = search_node(node, Prism::StatementsNode)
32
+ body_node = search_node(node, Prism::StatementsNode)
33
33
 
34
- return raw ?
35
- extract_raw_string_node(body_node) :
36
- node_to_s(body_node)
34
+ raw ? extract_raw_string_node(body_node) : node_to_s(body_node)
37
35
  end
38
36
 
39
37
  def extract_raw_string_node(node)
40
- search_node(node, Prism::InterpolatedStringNode)&.parts&.map(&:unescaped)&.join("") ||
41
- search_node(node, Prism::StringNode).unescaped
38
+ search_node(node, Prism::InterpolatedStringNode)&.parts&.map do |p|
39
+ p.respond_to?(:unescaped) ? p.unescaped : p.slice
40
+ end&.join("") ||
41
+ search_node(node, Prism::StringNode).unescaped
42
42
  end
43
43
 
44
-
45
44
  # Simple helper function to turn a SyntaxTree node back into a Ruby string
46
45
  # The default formatter will turn a break/return of [1,2,3] into a brackless 1,2,3
47
46
  # Can't have that in Crystal as it turns it into a Tuple
48
47
  def node_to_s(node)
49
- node&.slice || ''
48
+ node&.slice || ""
50
49
  end
51
50
 
52
51
  # Given a method, extracts the source code of the block passed to it
@@ -69,14 +68,14 @@ module CrystalRuby
69
68
  def extract_args_and_source_from_method(method, raw: false)
70
69
  method_source = extract_expr_from_source_location(method.source_location)
71
70
  parsed_source = Prism.parse(method_source).value
72
- params = search_node(parsed_source, Prism::ParametersNode)
73
- args = params ? params.keywords.map{|kw| [kw.name, node_to_s(kw.value)] }.to_h : {}
74
- body_node = parsed_source.statements.body[0].body
71
+ params = search_node(parsed_source, Prism::ParametersNode)
72
+ args = params ? params.keywords.map { |kw| [kw.name, node_to_s(kw.value)] }.to_h : {}
73
+ body_node = parsed_source.statements.body[0].body
75
74
  if body_node.respond_to?(:rescue_clause) && body_node.rescue_clause
76
- wrapped = %{begin\n#{body_node.statements.slice}\n#{body_node.rescue_clause.slice}\nend}
75
+ wrapped = %(begin\n#{body_node.statements.slice}\n#{body_node.rescue_clause.slice}\nend)
77
76
  body_node = Prism.parse(wrapped).value
78
77
  end
79
- body = raw ? extract_raw_string_node(body_node) : node_to_s(body_node)
78
+ body = raw ? extract_raw_string_node(body_node) : node_to_s(body_node)
80
79
 
81
80
  args.transform_values! do |type_exp|
82
81
  if CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP.key?(type_exp[1..-1].to_sym)
@@ -85,8 +84,7 @@ module CrystalRuby
85
84
  TypeBuilder.build_from_source(type_exp, context: method.owner)
86
85
  end
87
86
  end.to_h
88
- return args, body
87
+ [args, body]
89
88
  end
90
-
91
89
  end
92
90
  end
@@ -27,7 +27,7 @@ module CrystalRuby
27
27
  argv_ptr = ARGV1.to_unsafe
28
28
  Crystal.main_user_code(0, pointerof(argv_ptr))
29
29
  self.libname = String.new(libname)
30
- LibGC.set_finalize_on_demand(1)
30
+ GC.init
31
31
  end
32
32
 
33
33
  # Explicit error handling (triggers exception within Ruby on the same thread)
@@ -100,6 +100,11 @@ module GC
100
100
  end
101
101
  end
102
102
 
103
+ # Trigger GC
104
+ fun gc : Void
105
+ GC.collect
106
+ end
107
+
103
108
  # Yield to the Crystal scheduler from Ruby
104
109
  # If there's callbacks to process, we flush them
105
110
  # Otherwise, we yield to the Crystal scheduler and let Ruby know
@@ -3,31 +3,30 @@
3
3
  module CrystalRuby
4
4
  module Typemaps
5
5
  CRYSTAL_TYPE_MAP = {
6
- char: "Int8", # In Crystal, :char is typically represented as Int8
7
- uchar: "UInt8", # Unsigned char
8
- int8: "Int8", # Same as :char
9
- uint8: "UInt8", # Same as :uchar
10
- short: "Int16", # Short integer
11
- ushort: "UInt16", # Unsigned short integer
12
- int16: "Int16", # Same as :short
13
- uint16: "UInt16", # Same as :ushort
14
- int: "Int32", # Integer, Crystal defaults to 32 bits
15
- uint: "UInt32", # Unsigned integer
16
- int32: "Int32", # 32-bit integer
17
- uint32: "UInt32", # 32-bit unsigned integer
18
- long: "Int32 | Int64", # Long integer, size depends on the platform (32 or 64 bits)
19
- ulong: "UInt32 | UInt64", # Unsigned long integer, size depends on the platform
20
- int64: "Int64", # 64-bit integer
21
- uint64: "UInt64", # 64-bit unsigned integer
22
- long_long: "Int64", # Same as :int64
23
- ulong_long: "UInt64", # Same as :uint64
24
- float: "Float32", # Floating point number (single precision)
25
- double: "Float64", # Double precision floating point number
26
- bool: "Bool", # Boolean type
27
- void: "Void", # Void type
28
- string: "String", # String type
29
- pointer: "Pointer(Void)" # Pointer type
30
-
6
+ char: "::Int8", # In Crystal, :char is typically represented as Int8
7
+ uchar: "::UInt8", # Unsigned char
8
+ int8: "::Int8", # Same as :char
9
+ uint8: "::UInt8", # Same as :uchar
10
+ short: "::Int16", # Short integer
11
+ ushort: "::UInt16", # Unsigned short integer
12
+ int16: "::Int16", # Same as :short
13
+ uint16: "::UInt16", # Same as :ushort
14
+ int: "::Int32", # Integer, Crystal defaults to 32 bits
15
+ uint: "::UInt32", # Unsigned integer
16
+ int32: "::Int32", # 32-bit integer
17
+ uint32: "::UInt32", # 32-bit unsigned integer
18
+ long: "::Int32 | Int64", # Long integer, size depends on the platform (32 or 64 bits)
19
+ ulong: "::UInt32 | UInt64", # Unsigned long integer, size depends on the platform
20
+ int64: "::Int64", # 64-bit integer
21
+ uint64: "::UInt64", # 64-bit unsigned integer
22
+ long_long: "::Int64", # Same as :int64
23
+ ulong_long: "::UInt64", # Same as :uint64
24
+ float: "::Float32", # Floating point number (single precision)
25
+ double: "::Float64", # Double precision floating point number
26
+ bool: "::Bool", # Boolean type
27
+ void: "::Void", # Void type
28
+ string: "::String", # String type
29
+ pointer: "::Pointer(Void)" # Pointer type
31
30
  }
32
31
 
33
32
  FFI_TYPE_MAP = CRYSTAL_TYPE_MAP.invert
@@ -61,13 +60,13 @@ module CrystalRuby
61
60
 
62
61
  C_TYPE_MAP = CRYSTAL_TYPE_MAP.merge(
63
62
  {
64
- string: "Pointer(UInt8)"
63
+ string: "Pointer(::UInt8)"
65
64
  }
66
65
  )
67
66
 
68
67
  C_TYPE_CONVERSIONS = {
69
68
  string: {
70
- from: "String.new(%s)",
69
+ from: "::String.new(%s.not_nil!)",
71
70
  to: "%s.to_unsafe"
72
71
  },
73
72
  void: {
@@ -86,6 +85,11 @@ module CrystalRuby
86
85
 
87
86
  def build_type_map(crystalruby_type)
88
87
  crystalruby_type = CRType(&crystalruby_type) if crystalruby_type.is_a?(Proc)
88
+
89
+ if Types::Type.subclass?(crystalruby_type) && crystalruby_type.ffi_primitive_type
90
+ crystalruby_type = crystalruby_type.ffi_primitive_type
91
+ end
92
+
89
93
  {
90
94
  ffi_type: ffi_type(crystalruby_type),
91
95
  ffi_ret_type: ffi_type(crystalruby_type),
@@ -93,19 +97,25 @@ module CrystalRuby
93
97
  crystalruby_type: crystalruby_type,
94
98
  lib_type: lib_type(crystalruby_type),
95
99
  error_value: error_value(crystalruby_type),
96
- arg_mapper: if crystalruby_type.is_a?(Class) && crystalruby_type < Types::Type
100
+ arg_mapper: if Types::Type.subclass?(crystalruby_type)
97
101
  lambda { |arg|
98
102
  arg = crystalruby_type.new(arg.memory) if arg.is_a?(Types::Type) && !arg.is_a?(crystalruby_type)
99
103
  arg = crystalruby_type.new(arg) unless arg.is_a?(Types::Type)
104
+
105
+ Types::FixedWidth.increment_ref_count!(arg.memory) if arg.class < Types::FixedWidth
106
+
100
107
  arg
101
108
  }
102
109
  end,
103
- retval_mapper: if crystalruby_type.is_a?(Class) && crystalruby_type < Types::Type
110
+ retval_mapper: if Types::Type.subclass?(crystalruby_type)
104
111
  lambda { |arg|
105
112
  if arg.is_a?(Types::Type) && !arg.is_a?(crystalruby_type)
106
113
  arg = crystalruby_type.new(arg.memory)
107
114
  end
108
115
  arg = crystalruby_type.new(arg) unless arg.is_a?(Types::Type)
116
+
117
+ Types::FixedWidth.decrement_ref_count!(arg.memory) if arg.class < Types::FixedWidth
118
+
109
119
  crystalruby_type.anonymous? ? arg.native : arg
110
120
  }
111
121
  # Strings in Crystal are UTF-8 encoded by default
@@ -2,9 +2,21 @@ module CrystalRuby
2
2
  module Types
3
3
  # Module for memory allocation and tracking functionality
4
4
  module Allocator
5
-
6
5
  # Called when module is included in a class
7
6
  # @param base [Class] The class including this module
7
+
8
+ def self.gc_hint!(size)
9
+ @bytes_seen_since_gc = (@bytes_seen_since_gc || 0) + size
10
+ end
11
+
12
+ def self.gc_bytes_seen
13
+ @bytes_seen_since_gc ||= 0
14
+ end
15
+
16
+ def self.gc_hint_reset!
17
+ @bytes_seen_since_gc = 0
18
+ end
19
+
8
20
  def self.included(base)
9
21
  base.class_eval do
10
22
  # Synchronizes a block using mutex
@@ -21,7 +33,7 @@ module CrystalRuby
21
33
 
22
34
  extend FFI::Library
23
35
  ffi_lib "c"
24
- attach_function :_calloc, :calloc, [:size_t, :size_t], :pointer
36
+ attach_function :_calloc, :calloc, %i[size_t size_t], :pointer
25
37
  attach_function :_free, :free, [:pointer], :void
26
38
  define_singleton_method(:ptr, &FFI::Pointer.method(:new))
27
39
  define_method(:ptr, &FFI::Pointer.method(:new))
@@ -4,7 +4,8 @@ module CrystalRuby::Types
4
4
  "and a single return type (or Nil if it does not return a value)")
5
5
 
6
6
  def self.Proc(*types)
7
- proc_type = FixedWidth.build(:Proc, convert_if: [::Proc], inner_types: types, ffi_type: :pointer) do
7
+ proc_type = FixedWidth.build(:Proc, convert_if: [::Proc], inner_types: types,
8
+ ffi_type: :pointer) do
8
9
  @data_offset = 4
9
10
 
10
11
  def self.cast!(rbval)
@@ -57,12 +58,12 @@ module CrystalRuby::Types
57
58
  result = nil
58
59
  if Fiber.current == Thread.current.main_fiber
59
60
  block_value = #{inner_types[-1].crystal_class_name}.new(__yield_to.call(#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")}))
60
- result = #{inner_types[-1].anonymous? ? "block_value.native" : "block_value"}
61
+ result = #{inner_types[-1].anonymous? ? "block_value.native_decr" : "block_value"}
61
62
  next #{inner_types.last == CrystalRuby::Types::Nil ? "result" : "result.not_nil!"}
62
63
  else
63
64
  CrystalRuby.queue_callback(->{
64
65
  block_value = #{inner_types[-1].crystal_class_name}.new(__yield_to.call(#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")}))
65
- result = #{inner_types[-1].anonymous? ? "block_value.native" : "block_value"}
66
+ result = #{inner_types[-1].anonymous? ? "block_value.native_decr" : "block_value"}
66
67
  callback_done_channel.send(nil)
67
68
  })
68
69
  end
@@ -57,6 +57,11 @@ module CrystalRuby::Types
57
57
  union_types
58
58
  end
59
59
 
60
+ def total_memsize
61
+ type = self.class.union_types[data_pointer.read_uint8]
62
+ memsize + refsize + (type.primitive? ? type.memsize : value.total_memsize)
63
+ end
64
+
60
65
  define_singleton_method(:memsize) do
61
66
  union_types.map(&:refsize).max + 1
62
67
  end
@@ -35,6 +35,17 @@ module CrystalRuby
35
35
  8
36
36
  end
37
37
 
38
+ def self.new_decr(arg)
39
+ new_value = self.new(arg)
40
+ self.decrement_ref_count!(new_value.memory)
41
+ new_value
42
+ end
43
+
44
+ def native_decr
45
+ self.class.decrement_ref_count!(@memory)
46
+ native
47
+ end
48
+
38
49
 
39
50
  def self.free!(memory)
40
51
  # Decrease ref counts for any data we are pointing to
@@ -66,6 +77,13 @@ module CrystalRuby
66
77
  memory.as(Pointer(::UInt32))[0] = value
67
78
  end
68
79
 
80
+ # When we pass to Ruby, we increment the ref count
81
+ # for Ruby to decrement again once it receives.
82
+ def return_value
83
+ FixedWidth.increment_ref_count!(memory)
84
+ memory
85
+ end
86
+
69
87
  # Data pointer follows the ref count (and size for variable width types)
70
88
  # In the case of variable width types the data pointer points to the start of a separate data block
71
89
  # So this method is overridden inside variable_width.rb to resolve this pointer.
@@ -12,13 +12,14 @@ module CrystalRuby
12
12
  else allocate_new_from_value!(rbval)
13
13
  end
14
14
  self.class.increment_ref_count!(memory)
15
- ObjectSpace.define_finalizer(self, self.class.finalize(memory))
15
+ ObjectSpace.define_finalizer(self, self.class.finalize(memory, self.class))
16
+ Allocator.gc_hint!(total_memsize)
16
17
  end
17
18
 
18
- def self.finalize(memory)
19
- lambda { |_|
19
+ def self.finalize(memory, type)
20
+ lambda do |_|
20
21
  decrement_ref_count!(memory)
21
- }
22
+ end
22
23
  end
23
24
 
24
25
  def allocate_new_from_value!(rbval)
@@ -137,6 +138,10 @@ module CrystalRuby
137
138
  memory[size_offset].read_int32
138
139
  end
139
140
 
141
+ def total_memsize
142
+ memsize + refsize + size
143
+ end
144
+
140
145
  def address
141
146
  @memory.address
142
147
  end
@@ -180,6 +185,7 @@ module CrystalRuby
180
185
  superclass: FixedWidth,
181
186
  size_offset: 4,
182
187
  data_offset: 4,
188
+ ffi_primitive: false,
183
189
  &block
184
190
  )
185
191
  inner_types&.each(&Type.method(:validate!))
@@ -187,7 +193,7 @@ module CrystalRuby
187
193
  Class.new(superclass) do
188
194
  bind_local_vars!(
189
195
  %i[typename error inner_types inner_keys ffi_type memsize convert_if size_offset data_offset
190
- refsize], binding
196
+ refsize ffi_primitive], binding
191
197
  )
192
198
  class_eval(&block) if block_given?
193
199
 
@@ -60,11 +60,12 @@ module CrystalRuby
60
60
  memsize: FFI.type_size(ffi_type),
61
61
  convert_if: [],
62
62
  error: nil,
63
+ ffi_primitive: false,
63
64
  superclass: Primitive,
64
65
  &block
65
66
  )
66
67
  Class.new(superclass) do
67
- %w[typename ffi_type memsize convert_if error].each do |name|
68
+ %w[typename ffi_type memsize convert_if error ffi_primitive].each do |name|
68
69
  define_singleton_method(name) { binding.local_variable_get("#{name}") }
69
70
  define_method(name) { binding.local_variable_get("#{name}") }
70
71
  end
@@ -1,7 +1,7 @@
1
1
  module CrystalRuby::Types
2
2
  %i[UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64].each do |type_name|
3
- ffi_type = CrystalRuby::Typemaps::FFI_TYPE_MAP.fetch(type_name.to_s)
4
- const_set(type_name, Primitive.build(type_name, convert_if: [::Numeric], ffi_type: ffi_type) do
3
+ ffi_type = CrystalRuby::Typemaps::FFI_TYPE_MAP.fetch("::#{type_name}")
4
+ const_set(type_name, Primitive.build(type_name, convert_if: [::Numeric], ffi_type: ffi_type, ffi_primitive: ffi_type) do
5
5
  def value=(val)
6
6
  raise "Expected a numeric value, got #{val}" unless val.is_a?(::Numeric)
7
7
 
@@ -30,6 +30,10 @@ module CrystalRuby::Types
30
30
  define_singleton_method(:type_digest) do
31
31
  Digest::MD5.hexdigest(native_type_expr.to_s + allowed_values.map(&:to_s).join(","))
32
32
  end
33
+
34
+ def self.ffi_primitive_type
35
+ nil
36
+ end
33
37
  end
34
38
  end
35
39
  end
@@ -17,6 +17,14 @@ module CrystalRuby
17
17
  def to_s
18
18
  native.to_s
19
19
  end
20
+
21
+ def self.new_decr(arg)
22
+ self.new(arg)
23
+ end
24
+
25
+ def native_decr
26
+ native
27
+ end
20
28
 
21
29
  def synchronize
22
30
  CrystalRuby.synchronize do
@@ -47,7 +47,7 @@ module CrystalRuby
47
47
  :write_mixed_byte_slices_to_uint8_array, :data_offset, :size_offset,
48
48
  :union_types
49
49
 
50
- attr_accessor :value, :memory
50
+ attr_accessor :value, :memory, :ffi_primitive
51
51
 
52
52
  def initialize(_rbval)
53
53
  @class = self.class
@@ -133,7 +133,7 @@ module CrystalRuby
133
133
  end
134
134
 
135
135
  def self.pointer_to_crystal_type_conversion(expr)
136
- anonymous? ? "#{crystal_class_name}.new(#{expr}).native" : "#{crystal_class_name}.new(#{expr})"
136
+ anonymous? ? "#{crystal_class_name}.new(#{expr}).native_decr" : "#{crystal_class_name}.new_decr(#{expr})"
137
137
  end
138
138
 
139
139
  def self.crystal_type_to_pointer_type_conversion(expr)
@@ -243,6 +243,17 @@ module CrystalRuby
243
243
  inner_types.map(&:memsize).sum
244
244
  end
245
245
 
246
+ def total_memsize
247
+ memsize
248
+ end
249
+
250
+ # For non-container ffi_primitive non-named types,
251
+ # just use the raw FFI type, as it's much more efficient
252
+ # due to skipping Arc overhead.
253
+ def self.ffi_primitive_type
254
+ respond_to?(:ffi_primitive) && anonymous? ? ffi_primitive : nil
255
+ end
256
+
246
257
  def self.crystal_type
247
258
  lib_type(ffi_type)
248
259
  end
@@ -265,6 +276,10 @@ module CrystalRuby
265
276
  inner_types.first
266
277
  end
267
278
 
279
+ def self.subclass?(type)
280
+ type.is_a?(Class) && type < Types::Type
281
+ end
282
+
268
283
  def self.type_expr
269
284
  if !inner_types
270
285
  inspect_name
@@ -1,5 +1,5 @@
1
1
  module CrystalRuby::Types
2
- String = VariableWidth.build(:String, convert_if: [String, Root::String]) do
2
+ String = VariableWidth.build(:String, ffi_primitive: :string, convert_if: [String, Root::String]) do
3
3
  def self.cast!(rbval)
4
4
  rbval.to_s
5
5
  end
@@ -19,6 +19,7 @@ module CrystalRuby
19
19
  inner_types: nil,
20
20
  inner_keys: nil,
21
21
  ffi_type: :pointer,
22
+ ffi_primitive: false,
22
23
  size_offset: 4,
23
24
  data_offset: 8,
24
25
  memsize: FFI.type_size(ffi_type),
@@ -32,7 +33,7 @@ module CrystalRuby
32
33
  Class.new(superclass) do
33
34
  bind_local_vars!(
34
35
  %i[typename error inner_types inner_keys ffi_type memsize convert_if data_offset size_offset
35
- refsize], binding
36
+ refsize ffi_primitive], binding
36
37
  )
37
38
  class_eval(&block) if block_given?
38
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CrystalRuby
4
- VERSION = "0.3.2"
4
+ VERSION = "0.3.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crystalruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-22 00:00:00.000000000 Z
11
+ date: 2025-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: digest
@@ -42,30 +42,30 @@ dependencies:
42
42
  name: fileutils
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '1.7'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '1.7'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: prism
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">="
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: 1.3.0
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ">="
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: 1.3.0
69
69
  description: Embed Crystal code directly in Ruby.
70
70
  email:
71
71
  - wc@pico.net.nz