crystalruby 0.2.3 → 0.3.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/README.md +389 -193
  4. data/Rakefile +4 -3
  5. data/crystalruby.gemspec +2 -2
  6. data/exe/crystalruby +1 -0
  7. data/lib/crystalruby/adapter.rb +131 -65
  8. data/lib/crystalruby/arc_mutex.rb +47 -0
  9. data/lib/crystalruby/compilation.rb +32 -3
  10. data/lib/crystalruby/config.rb +41 -37
  11. data/lib/crystalruby/function.rb +211 -68
  12. data/lib/crystalruby/library.rb +153 -48
  13. data/lib/crystalruby/reactor.rb +40 -23
  14. data/lib/crystalruby/source_reader.rb +86 -0
  15. data/lib/crystalruby/template.rb +16 -5
  16. data/lib/crystalruby/templates/function.cr +11 -10
  17. data/lib/crystalruby/templates/index.cr +53 -66
  18. data/lib/crystalruby/templates/inline_chunk.cr +1 -1
  19. data/lib/crystalruby/templates/ruby_interface.cr +34 -0
  20. data/lib/crystalruby/templates/top_level_function.cr +62 -0
  21. data/lib/crystalruby/templates/top_level_ruby_interface.cr +33 -0
  22. data/lib/crystalruby/typebuilder.rb +11 -55
  23. data/lib/crystalruby/typemaps.rb +92 -67
  24. data/lib/crystalruby/types/concerns/allocator.rb +80 -0
  25. data/lib/crystalruby/types/fixed_width/named_tuple.cr +80 -0
  26. data/lib/crystalruby/types/fixed_width/named_tuple.rb +86 -0
  27. data/lib/crystalruby/types/fixed_width/proc.cr +45 -0
  28. data/lib/crystalruby/types/fixed_width/proc.rb +79 -0
  29. data/lib/crystalruby/types/fixed_width/tagged_union.cr +53 -0
  30. data/lib/crystalruby/types/fixed_width/tagged_union.rb +109 -0
  31. data/lib/crystalruby/types/fixed_width/tuple.cr +82 -0
  32. data/lib/crystalruby/types/fixed_width/tuple.rb +92 -0
  33. data/lib/crystalruby/types/fixed_width.cr +138 -0
  34. data/lib/crystalruby/types/fixed_width.rb +205 -0
  35. data/lib/crystalruby/types/primitive.cr +21 -0
  36. data/lib/crystalruby/types/primitive.rb +117 -0
  37. data/lib/crystalruby/types/primitive_types/bool.cr +34 -0
  38. data/lib/crystalruby/types/primitive_types/bool.rb +11 -0
  39. data/lib/crystalruby/types/primitive_types/nil.cr +35 -0
  40. data/lib/crystalruby/types/primitive_types/nil.rb +16 -0
  41. data/lib/crystalruby/types/primitive_types/numbers.cr +37 -0
  42. data/lib/crystalruby/types/primitive_types/numbers.rb +28 -0
  43. data/lib/crystalruby/types/primitive_types/symbol.cr +55 -0
  44. data/lib/crystalruby/types/primitive_types/symbol.rb +35 -0
  45. data/lib/crystalruby/types/primitive_types/time.cr +35 -0
  46. data/lib/crystalruby/types/primitive_types/time.rb +25 -0
  47. data/lib/crystalruby/types/type.cr +64 -0
  48. data/lib/crystalruby/types/type.rb +239 -30
  49. data/lib/crystalruby/types/variable_width/array.cr +74 -0
  50. data/lib/crystalruby/types/variable_width/array.rb +88 -0
  51. data/lib/crystalruby/types/variable_width/hash.cr +146 -0
  52. data/lib/crystalruby/types/variable_width/hash.rb +117 -0
  53. data/lib/crystalruby/types/variable_width/string.cr +36 -0
  54. data/lib/crystalruby/types/variable_width/string.rb +18 -0
  55. data/lib/crystalruby/types/variable_width.cr +23 -0
  56. data/lib/crystalruby/types/variable_width.rb +46 -0
  57. data/lib/crystalruby/types.rb +32 -13
  58. data/lib/crystalruby/version.rb +2 -2
  59. data/lib/crystalruby.rb +13 -6
  60. metadata +41 -19
  61. data/lib/crystalruby/types/array.rb +0 -15
  62. data/lib/crystalruby/types/bool.rb +0 -3
  63. data/lib/crystalruby/types/hash.rb +0 -17
  64. data/lib/crystalruby/types/named_tuple.rb +0 -28
  65. data/lib/crystalruby/types/nil.rb +0 -3
  66. data/lib/crystalruby/types/numbers.rb +0 -5
  67. data/lib/crystalruby/types/string.rb +0 -3
  68. data/lib/crystalruby/types/symbol.rb +0 -3
  69. data/lib/crystalruby/types/time.rb +0 -8
  70. data/lib/crystalruby/types/tuple.rb +0 -17
  71. data/lib/crystalruby/types/type_serializer/json.rb +0 -41
  72. data/lib/crystalruby/types/type_serializer.rb +0 -37
  73. data/lib/crystalruby/types/typedef.rb +0 -57
  74. data/lib/crystalruby/types/union_type.rb +0 -43
  75. data/lib/module.rb +0 -3
@@ -1,13 +1,17 @@
1
1
  module CrystalRuby
2
+ require 'json'
2
3
  # The Reactor represents a singleton Thread responsible for running all Ruby/crystal interop code.
3
4
  # Crystal's Fiber scheduler and GC assume all code is run on a single thread.
4
- # This class is responsible for multiplexing Ruby and Crystal code on a single thread.
5
- # Functions annotated with async: true, are executed using callbacks to allow these to be interleaved without blocking.
6
-
5
+ # This class is responsible for multiplexing Ruby and Crystal code onto a single thread.
6
+ # Functions annotated with async: true, are executed using callbacks to allow these to be interleaved
7
+ # without blocking multiple Ruby threads.
7
8
  module Reactor
8
9
  module_function
9
10
 
10
11
  class SingleThreadViolation < StandardError; end
12
+ class StopReactor < StandardError; end
13
+
14
+ @single_thread_mode = false
11
15
 
12
16
  REACTOR_QUEUE = Queue.new
13
17
 
@@ -39,13 +43,15 @@ module CrystalRuby
39
43
  end
40
44
  end
41
45
 
42
- ERROR_CALLBACK = FFI::Function.new(:void, %i[string string int]) do |error_type, message, tid|
46
+ ERROR_CALLBACK = FFI::Function.new(:void, %i[string string string int]) do |error_type, message, backtrace, tid|
43
47
  error_type = error_type.to_sym
44
48
  is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception)
45
49
  error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
46
- raise error_type.new(message) unless THREAD_MAP.key?(tid)
50
+ error = error_type.new(message)
51
+ error.set_backtrace(JSON.parse(backtrace))
52
+ raise error unless THREAD_MAP.key?(tid)
47
53
 
48
- THREAD_MAP[tid][:error] = error_type.new(message)
54
+ THREAD_MAP[tid][:error] = error
49
55
  THREAD_MAP[tid][:result] = nil
50
56
  THREAD_MAP[tid][:cond].signal
51
57
  end
@@ -57,18 +63,35 @@ module CrystalRuby
57
63
  def await_result!
58
64
  mux, cond = thread_conditions.values_at(:mux, :cond)
59
65
  cond.wait(mux)
60
- raise THREAD_MAP[thread_id][:error] if THREAD_MAP[thread_id][:error]
66
+ if error = thread_conditions[:error]
67
+ combined_backtrace = error.backtrace[0..(error.backtrace.index{|m| m.include?('call_blocking_function')} || 2) - 3] + caller[5..-1]
68
+ error.set_backtrace(combined_backtrace)
69
+ raise error
70
+ end
71
+
72
+ thread_conditions[:result]
73
+ end
74
+
75
+ def halt_loop!
76
+ raise StopReactor
77
+ end
61
78
 
62
- THREAD_MAP[thread_id][:result]
79
+ def stop!
80
+ if @main_loop
81
+ schedule_work!(self, :halt_loop!, :void, blocking: true, async: false)
82
+ @main_loop.join
83
+ @main_loop = nil
84
+ CrystalRuby.log_info "Reactor loop stopped"
85
+ end
63
86
  end
64
87
 
65
88
  def start!
66
89
  @main_loop ||= Thread.new do
90
+ @main_thread_id = Thread.current.object_id
67
91
  CrystalRuby.log_debug("Starting reactor")
68
92
  CrystalRuby.log_debug("CrystalRuby initialized")
69
- loop do
70
- REACTOR_QUEUE.pop[]
71
- end
93
+ REACTOR_QUEUE.pop[] while true
94
+ rescue StopReactor => e
72
95
  rescue StandardError => e
73
96
  CrystalRuby.log_error "Error: #{e}"
74
97
  CrystalRuby.log_error e.backtrace
@@ -80,26 +103,17 @@ module CrystalRuby
80
103
  end
81
104
 
82
105
  def yield!(lib: nil, time: 0.0)
83
- schedule_work!(lib, :yield, nil, async: false, blocking: false, lib: lib) if running? && lib
106
+ schedule_work!(lib, :yield, :int, async: false, blocking: false, lib: lib) if running? && lib
84
107
  nil
85
108
  end
86
109
 
87
- def current_thread_id=(val)
88
- @current_thread_id = val
89
- end
90
-
91
- def current_thread_id
92
- @current_thread_id
93
- end
94
-
95
110
  def schedule_work!(receiver, op_name, *args, return_type, blocking: true, async: true, lib: nil)
96
- if @single_thread_mode
111
+ if @single_thread_mode || (Thread.current.object_id == @main_thread_id && op_name != :yield)
97
112
  unless Thread.current.object_id == @main_thread_id
98
113
  raise SingleThreadViolation,
99
114
  "Single thread mode is enabled, cannot run in multi-threaded mode. " \
100
115
  "Reactor was started from: #{@main_thread_id}, then called from #{Thread.current.object_id}"
101
116
  end
102
-
103
117
  return receiver.send(op_name, *args)
104
118
  end
105
119
 
@@ -118,14 +132,17 @@ module CrystalRuby
118
132
  when blocking
119
133
  lambda {
120
134
  tvars[:error] = nil
121
- Reactor.current_thread_id = tvars[:thread_id]
135
+ should_halt = false
122
136
  begin
123
137
  result = receiver.send(op_name, *args)
138
+ rescue StopReactor => e
139
+ should_halt = true
124
140
  rescue StandardError => e
125
141
  tvars[:error] = e
126
142
  end
127
143
  tvars[:result] = result unless tvars[:error]
128
144
  tvars[:cond].signal
145
+ raise StopReactor if should_halt
129
146
  }
130
147
  else
131
148
  lambda {
@@ -0,0 +1,86 @@
1
+ module CrystalRuby
2
+ module SourceReader
3
+ module_function
4
+
5
+ # Reads code line by line from a given source location and returns the first valid Ruby expression found
6
+ def extract_expr_from_source_location(source_location)
7
+ lines = source_location.then{|f,l| IO.readlines(f)[l-1..]}
8
+ lines[0] = lines[0][/CRType.*/] if lines[0] =~ /<\s+CRType/ || lines[0] =~ /= CRType/
9
+ lines.each.with_object("") do |line, expr_source|
10
+ break expr_source if (SyntaxTree.parse(expr_source << line) rescue nil)
11
+ end
12
+ rescue
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
+ end
15
+
16
+ # Given a proc, extracts the source code of the block passed to it
17
+ # If raw is true, the source is expected to be Raw Crystal code captured
18
+ # in a string or Heredoc literal. Otherwise the Ruby code (assumed to be valid Crystal)
19
+ # is extracted.
20
+ def extract_source_from_proc(block, raw: false)
21
+ block_source = extract_expr_from_source_location(block.source_location)
22
+ body_node = SyntaxTree.search(block_source, :Statements).to_a[1]
23
+ return raw ?
24
+ SyntaxTree.search(node_to_s(body_node), :TStringContent).map(&method(:node_to_s)).join :
25
+ node_to_s(body_node)
26
+ end
27
+
28
+ @visitor = SyntaxTree::MutationVisitor.new
29
+
30
+ # Specify that it should mutate If nodes with assignments in their predicates
31
+ @visitor.mutate("ReturnNode") do |node|
32
+ node
33
+ end
34
+
35
+ # Simple helper function to turn a SyntaxTree node back into a Ruby string
36
+ # The default formatter will turn a break/return of [1,2,3] into a brackless 1,2,3
37
+ # Can't have that in Crystal as it turns it into a Tuple
38
+ def node_to_s(node)
39
+ @_syxt_transform ||= SyntaxTree::FlowControlFormatter.prepend(Module.new do
40
+ def format(quer)
41
+ first_arg = self.node.arguments.child_nodes
42
+ if first_arg[0].kind_of?(SyntaxTree::ArrayLiteral)
43
+ return format_arguments(quer, " [", "]")
44
+ end
45
+ super(quer)
46
+ end
47
+ end)
48
+ SyntaxTree::Formatter.format("", node.accept(@visitor))
49
+ end
50
+
51
+ # Given a method, extracts the source code of the block passed to it
52
+ # and also converts any keyword arguments given in the method definition as a
53
+ # named map of keyword names to Crystal types.
54
+ # Also supports basic ffi symbol types.
55
+ #
56
+ # E.g.
57
+ #
58
+ # def add a: Int32 | Int64, b: :int
59
+ #
60
+ # The above will be converted to:
61
+ # {
62
+ # a: Int32 | Int64, # Int32 | Int64 is a Crystal type
63
+ # b: :int # :int is an FFI type shorthand
64
+ # }
65
+ # If raw is true, the source is expected to be Raw Crystal code captured
66
+ # in a string or Heredoc literal. Otherwise the Ruby code (assumed to be valid Crystal)
67
+ # is extracted.
68
+ def extract_args_and_source_from_method(method, raw: false)
69
+ method_source = extract_expr_from_source_location(method.source_location)
70
+ params = SyntaxTree.search(method_source, :Params).first
71
+ args = params ? params.keywords.map{|k,v| [k.value[0...-1].to_sym, node_to_s(v)] }.to_h : {}
72
+ body_node = SyntaxTree.search(method_source, :BodyStmt).first || SyntaxTree.search(method_source, :Statements).to_a[1]
73
+ body = node_to_s(body_node)
74
+ body = SyntaxTree.search(body, :TStringContent).map(&method(:node_to_s)).join if raw
75
+ args.transform_values! do |type_exp|
76
+ if CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP.key?(type_exp[1..-1].to_sym)
77
+ type_exp[1..-1].to_sym
78
+ else
79
+ TypeBuilder.build_from_source(type_exp, context: method.owner)
80
+ end
81
+ end.to_h
82
+ return args, body
83
+ end
84
+
85
+ end
86
+ end
@@ -1,12 +1,23 @@
1
1
  module CrystalRuby
2
2
  module Template
3
- Dir[File.join(File.dirname(__FILE__), "templates", "*.cr")].each do |file|
3
+ class Renderer < Struct.new(:raw_value)
4
+ require 'erb'
5
+ def render(context)
6
+ if context.kind_of?(::Hash)
7
+ raw_value % context
8
+ else
9
+ ERB.new(raw_value, trim_mode: "%").result(context)
10
+ end
11
+ end
12
+ end
13
+
14
+ (
15
+ Dir[File.join(File.dirname(__FILE__), "templates", "**", "*.cr")] +
16
+ Dir[File.join(File.dirname(__FILE__), "types", "**", "*.cr")]
17
+ ).each do |file|
4
18
  template_name = File.basename(file, File.extname(file)).split("_").map(&:capitalize).join
5
19
  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)
20
+ const_set(template_name, Renderer.new(template_value))
10
21
  end
11
22
  end
12
23
  end
@@ -3,8 +3,8 @@
3
3
  # Crystal code can simply call this method directly, enabling generated crystal code
4
4
  # to call other generated crystal code without overhead.
5
5
 
6
- module %{module_name}
7
- def self.%{fn_name}(%{fn_args}) : %{fn_ret_type}
6
+ %{module_or_class} %{module_name} %{superclass}
7
+ def %{fn_scope}%{fn_name}(%{fn_args}) : %{fn_ret_type}
8
8
  %{fn_body}
9
9
  end
10
10
  end
@@ -16,13 +16,13 @@ fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type}
16
16
  begin
17
17
  %{convert_lib_args}
18
18
  begin
19
- return_value = %{module_name}.%{fn_name}(%{arg_names})
19
+ return_value = %{receiver}.%{fn_name}(%{arg_names})%{block_converter}
20
20
  return %{convert_return_type}
21
21
  rescue ex
22
- CrystalRuby.report_error("RuntimeError", ex.message.to_s, 0)
22
+ CrystalRuby.report_error("RuntimeError", ex.message.to_s, ex.backtrace.to_json, 0)
23
23
  end
24
24
  rescue ex
25
- CrystalRuby.report_error("ArgumentError", ex.message.to_s, 0)
25
+ CrystalRuby.report_error("ArgumentError", ex.message.to_s, ex.backtrace.to_json, 0)
26
26
  end
27
27
  return %{error_value}
28
28
  end
@@ -37,25 +37,26 @@ fun %{lib_fn_name}_async(%{lib_fn_args} thread_id: UInt32, callback : %{callbac
37
37
  CrystalRuby.increment_task_counter
38
38
  spawn do
39
39
  begin
40
- return_value = %{module_name}.%{fn_name}(%{arg_names})
41
- converted = %{convert_return_type}
40
+ return_value = %{receiver}.%{fn_name}(%{arg_names})%{block_converter}
42
41
  CrystalRuby.queue_callback(->{
42
+ converted = %{convert_return_type}
43
43
  %{callback_call}
44
44
  CrystalRuby.decrement_task_counter
45
45
  })
46
46
  rescue ex
47
47
  exception = ex.message.to_s
48
+ backtrace = ex.backtrace.to_json
48
49
  CrystalRuby.queue_callback(->{
49
- CrystalRuby.error_callback.call("RuntimeError".to_unsafe, exception.to_unsafe, thread_id)
50
+ CrystalRuby.report_error("RuntimeError", exception, backtrace, thread_id)
50
51
  CrystalRuby.decrement_task_counter
51
52
  })
52
53
  end
53
54
  end
54
55
  rescue ex
55
-
56
56
  exception = ex.message.to_s
57
+ backtrace = ex.backtrace.to_json
57
58
  CrystalRuby.queue_callback(->{
58
- CrystalRuby.error_callback.call("RuntimeError".to_unsafe, ex.message.to_s.to_unsafe, thread_id)
59
+ CrystalRuby.report_error("RuntimeError", ex.message.to_s, backtrace, thread_id)
59
60
  CrystalRuby.decrement_task_counter
60
61
  })
61
62
  end
@@ -1,48 +1,40 @@
1
1
  module CrystalRuby
2
-
3
2
  ARGV1 = "crystalruby"
4
- CALLBACK_MUX = Mutex.new
5
3
 
6
- alias ErrorCallback = (Pointer(UInt8), Pointer(UInt8), UInt32 -> Void)
4
+ alias ErrorCallback = (Pointer(::UInt8), Pointer(::UInt8), Pointer(::UInt8), ::UInt32 -> Void)
5
+
6
+ class_property libname : String = "crystalruby"
7
+ class_property callbacks : Channel(Proc(Nil)) = Channel(Proc(Nil)).new
8
+ class_property rc_mux : Pointer(Void) = Pointer(Void).null
9
+ class_property task_counter : Atomic(Int32) = Atomic(Int32).new(0)
7
10
 
8
11
  # Initializing Crystal Ruby invokes init on the Crystal garbage collector.
9
12
  # We need to be sure to only do this once.
10
- @@initialized = false
11
-
12
- @@libname = "crystalruby"
13
- # Our Ruby <-> Crystal Reactor uses Fibers, with callbacks to allow
14
- # multiple concurrent Crystal operations to be queued
15
- @@callbacks = [] of Proc(Nil)
16
-
17
- # We only continue to yield to the Crystal scheduler from Ruby
18
- # while there are outstanding tasks.
19
- @@task_counter : Atomic(Int32) = Atomic.new(0)
13
+ class_property initialized : Bool = false
20
14
 
21
15
  # We can override the error callback to catch errors in Crystal,
22
16
  # and explicitly expose them to Ruby.
23
- @@error_callback : ErrorCallback = ->(t : UInt8* , s : UInt8*, tid : UInt32){ puts "Error: #{t}:#{s}" }
17
+ @@error_callback : ErrorCallback?
24
18
 
25
19
  # This is the entry point for instantiating CrystalRuby
26
20
  # We:
27
21
  # 1. Initialize the Crystal garbage collector
28
22
  # 2. Set the error callback
29
23
  # 3. Call the Crystal main function
30
- def self.init(libname : Pointer(UInt8), error_callback : ErrorCallback)
31
- return if @@initialized
32
- @@initialized = true
24
+ def self.init(libname : Pointer(::UInt8), @@error_callback : ErrorCallback, @@rc_mux : Pointer(Void))
25
+ return if self.initialized
26
+ self.initialized = true
33
27
  argv_ptr = ARGV1.to_unsafe
34
28
  Crystal.main_user_code(0, pointerof(argv_ptr))
35
- @@error_callback = error_callback
36
- @@libname = String.new(libname)
29
+ self.libname = String.new(libname)
30
+ LibGC.set_finalize_on_demand(1)
37
31
  end
38
32
 
39
33
  # Explicit error handling (triggers exception within Ruby on the same thread)
40
- def self.report_error(error_type : String, str : String, thread_id : UInt32, )
41
- @@error_callback.call(error_type.to_unsafe, str.to_unsafe, thread_id)
42
- end
43
-
44
- def self.error_callback : ErrorCallback
45
- @@error_callback
34
+ def self.report_error(error_type : String, message : String, backtrace : String, thread_id : UInt32)
35
+ if error_reporter = @@error_callback
36
+ error_reporter.call(error_type.to_unsafe, message.to_unsafe, backtrace.to_unsafe, thread_id)
37
+ end
46
38
  end
47
39
 
48
40
  # New async task started
@@ -57,51 +49,40 @@ module CrystalRuby
57
49
 
58
50
  # Get number of outstanding tasks
59
51
  def self.get_task_counter : Int32
60
- @@task_counter.get()
52
+ @@task_counter.get
61
53
  end
62
54
 
63
55
  # Queue a callback for an async task
64
56
  def self.queue_callback(callback : Proc(Nil))
65
- CALLBACK_MUX.synchronize do
66
- @@callbacks << callback
67
- end
68
- end
69
-
70
- # Get number of queued callbacks
71
- def self.count_callbacks : Int32
72
- @@callbacks.size
57
+ self.callbacks.send(callback)
73
58
  end
74
59
 
75
- def self.libname : String
76
- @@libname
77
- end
78
-
79
- # Flush all callbacks
80
- def self.flush_callbacks : Int32
81
- CALLBACK_MUX.synchronize do
82
- count = @@callbacks.size
83
- @@callbacks.each do |callback|
84
- result = callback.call()
85
- end
86
- @@callbacks.clear
87
- end
88
- get_task_counter
60
+ def self.synchronize
61
+ LibC.pthread_mutex_lock(self.rc_mux)
62
+ yield
63
+ LibC.pthread_mutex_unlock(self.rc_mux)
89
64
  end
90
65
  end
91
66
 
92
67
  # Initialize CrystalRuby
93
- fun init(libname : Pointer(UInt8), cb : CrystalRuby::ErrorCallback): Void
94
- CrystalRuby.init(libname, cb)
68
+ fun init(libname : Pointer(::UInt8), cb : CrystalRuby::ErrorCallback, rc_mux : Pointer(Void)) : Void
69
+ CrystalRuby.init(libname, cb, rc_mux)
95
70
  end
96
71
 
97
72
  fun stop : Void
98
- LibGC.deinit()
73
+ LibGC.deinit
99
74
  end
100
75
 
101
76
  @[Link("gc")]
102
77
  lib LibGC
103
78
  $stackbottom = GC_stackbottom : Void*
104
79
  fun deinit = GC_deinit
80
+ fun set_finalize_on_demand = GC_set_finalize_on_demand(Int32)
81
+ fun invoke_finalizers = GC_invoke_finalizers : Int
82
+ end
83
+
84
+ lib LibC
85
+ fun calloc = calloc(Int32, Int32) : Void*
105
86
  end
106
87
 
107
88
  module GC
@@ -112,6 +93,11 @@ module GC
112
93
  def self.set_stackbottom(stack_bottom : Void*)
113
94
  LibGC.stackbottom = stack_bottom
114
95
  end
96
+
97
+ def self.collect
98
+ LibGC.collect
99
+ LibGC.invoke_finalizers
100
+ end
115
101
  end
116
102
 
117
103
  # Yield to the Crystal scheduler from Ruby
@@ -119,24 +105,25 @@ end
119
105
  # Otherwise, we yield to the Crystal scheduler and let Ruby know
120
106
  # how many outstanding tasks still remain (it will stop yielding to Crystal
121
107
  # once this figure reaches 0).
122
- fun yield() : Int32
123
- if CrystalRuby.count_callbacks == 0
124
- Fiber.yield
125
-
126
- # TODO: We should apply backpressure here to prevent busy waiting if the number of outstanding tasks is not decreasing.
127
- # Use a simple exponential backoff strategy, to increase the time between each yield up to a maximum of 1 second.
128
-
129
- CrystalRuby.get_task_counter
130
- else
131
- CrystalRuby.flush_callbacks()
108
+ fun yield : Int32
109
+ Fiber.yield
110
+ loop do
111
+ select
112
+ when callback = CrystalRuby.callbacks.receive
113
+ callback.call
114
+ else
115
+ break
116
+ end
132
117
  end
118
+ CrystalRuby.get_task_counter
133
119
  end
134
120
 
121
+ class Array(T)
122
+ def initialize(size : Int32, @buffer : Pointer(T))
123
+ @size = size.to_i32
124
+ @capacity = @size
125
+ end
126
+ end
135
127
 
136
- # This is where we define all our Crystal modules and types
137
- # derived from their Ruby counterparts.
138
- %{type_modules}
139
-
140
- # Require all generated crystal files
141
128
  require "json"
142
129
  %{requires}
@@ -1,6 +1,6 @@
1
1
  # This is the template used for writing inline chunks of Crystal code without direct
2
2
  # Ruby integration
3
3
 
4
- module %{module_name}
4
+ %{mod_or_class} %{module_name} %{superclass}
5
5
  %{body}
6
6
  end
@@ -0,0 +1,34 @@
1
+ # This is the template used for all CrystalRuby functions
2
+ # Calls to this method *from ruby* are first transformed through the lib function.
3
+ # Crystal code can simply call this method directly, enabling generated crystal code
4
+ # to call other generated crystal code without overhead.
5
+
6
+ %{module_or_class} %{module_name} %{superclass}
7
+ def %{fn_scope}%{fn_name}(%{fn_args}) : %{fn_ret_type}
8
+ %{convert_lib_args}
9
+ cb = %{module_name}.%{callback_name}
10
+ unless cb.nil?
11
+ callback_done_channel = Channel(Nil).new
12
+ return_value = nil
13
+ if Fiber.current == Thread.current.main_fiber
14
+ return_value = cb.call(%{lib_fn_arg_names})
15
+ return %{convert_return_type}
16
+ else
17
+ CrystalRuby.queue_callback(->{
18
+ return_value = cb.call(%{lib_fn_arg_names})
19
+ callback_done_channel.send(nil)
20
+ })
21
+ end
22
+ callback_done_channel.receive
23
+ return %{convert_return_type}
24
+ end
25
+ raise "No callback registered for %{fn_name}"
26
+
27
+ end
28
+
29
+ class_property %{callback_name} : Proc(%{lib_fn_types} %{lib_fn_ret_type})?
30
+ end
31
+
32
+ fun register_%{callback_name}(callback : Proc(%{lib_fn_types} %{lib_fn_ret_type})) : Void
33
+ %{module_name}.%{callback_name} = callback
34
+ end
@@ -0,0 +1,62 @@
1
+ # This is the template used for all CrystalRuby functions
2
+ # Calls to this method *from ruby* are first transformed through the lib function.
3
+ # Crystal code can simply call this method directly, enabling generated crystal code
4
+ # to call other generated crystal code without overhead.
5
+
6
+ def %{fn_name}(%{fn_args}) : %{fn_ret_type}
7
+ %{fn_body}
8
+ end
9
+
10
+ # This function is the entry point for the CrystalRuby code, exposed through FFI.
11
+ # We apply some basic error handling here, and convert the arguments and return values
12
+ # to ensure that we are using Crystal native types.
13
+ fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type}
14
+ begin
15
+ %{convert_lib_args}
16
+ begin
17
+ return_value = %{fn_name}(%{arg_names})%{block_converter}
18
+ return %{convert_return_type}
19
+ rescue ex
20
+ CrystalRuby.report_error("RuntimeError", ex.message.to_s, ex.backtrace.to_json, 0)
21
+ end
22
+ rescue ex
23
+ CrystalRuby.report_error("ArgumentError", ex.message.to_s, ex.backtrace.to_json, 0)
24
+ end
25
+ return %{error_value}
26
+ end
27
+
28
+
29
+ # This function is the async entry point for the CrystalRuby code, exposed through FFI.
30
+ # We apply some basic error handling here, and convert the arguments and return values
31
+ # to ensure that we are using Crystal native types.
32
+ fun %{lib_fn_name}_async(%{lib_fn_args} thread_id: UInt32, callback : %{callback_type}): Void
33
+ begin
34
+ %{convert_lib_args}
35
+ CrystalRuby.increment_task_counter
36
+ spawn do
37
+ begin
38
+ return_value = %{fn_name}(%{arg_names})%{block_converter}
39
+ CrystalRuby.queue_callback(->{
40
+ converted = %{convert_return_type}
41
+ %{callback_call}
42
+ CrystalRuby.decrement_task_counter
43
+ })
44
+ rescue ex
45
+ exception = ex.message.to_s
46
+ backtrace = ex.backtrace.to_json
47
+ CrystalRuby.queue_callback(->{
48
+ CrystalRuby.report_error("RuntimeError", exception, backtrace, thread_id)
49
+ CrystalRuby.decrement_task_counter
50
+ })
51
+ end
52
+ end
53
+ rescue ex
54
+
55
+ exception = ex.message.to_s
56
+ backtrace = ex.backtrace.to_json
57
+ CrystalRuby.queue_callback(->{
58
+ CrystalRuby.report_error("RuntimeError", ex.message.to_s, backtrace, thread_id)
59
+ CrystalRuby.decrement_task_counter
60
+ })
61
+ end
62
+ end
@@ -0,0 +1,33 @@
1
+ # This is the template used for all CrystalRuby functions
2
+ # Calls to this method *from ruby* are first transformed through the lib function.
3
+ # Crystal code can simply call this method directly, enabling generated crystal code
4
+ # to call other generated crystal code without overhead.
5
+
6
+ module TopLevelCallbacks
7
+ class_property %{fn_name}_callback : Proc(%{lib_fn_types} %{lib_fn_ret_type})?
8
+ end
9
+
10
+ def %{fn_scope}%{fn_name}(%{fn_args}) : %{fn_ret_type}
11
+ %{convert_lib_args}
12
+ cb = TopLevelCallbacks.%{fn_name}_callback
13
+ unless cb.nil?
14
+ callback_done_channel = Channel(Nil).new
15
+ return_value = nil
16
+ if Fiber.current == Thread.current.main_fiber
17
+ return_value = cb.call(%{lib_fn_arg_names})
18
+ return %{convert_return_type}
19
+ else
20
+ CrystalRuby.queue_callback(->{
21
+ return_value = cb.call(%{lib_fn_arg_names})
22
+ callback_done_channel.send(nil)
23
+ })
24
+ end
25
+ callback_done_channel.receive
26
+ return %{convert_return_type}
27
+ end
28
+ raise "No callback registered for %{fn_name}"
29
+ end
30
+
31
+ fun register_%{fn_name}_callback(callback : Proc(%{lib_fn_types} %{lib_fn_ret_type})) : Void
32
+ TopLevelCallbacks.%{fn_name}_callback = callback
33
+ end