crystalruby 0.2.3 → 0.3.0

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