ffi-llvm-jit 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7330fb0bd311b6b6655240b9801e1af1951964f57210540c6d00db78aeddf001
4
- data.tar.gz: a98d9784adebf9e4c4ecdbeb778a48ffd2a19161713f5f8a48c5dc3b00abac58
3
+ metadata.gz: bfb626cce651accf4c6a89183521b2466b28896d51da8618e9bd5ba6d4cab0ea
4
+ data.tar.gz: f981dac4d5b6b28ca8deda9f8d83fac86fdba9f652341cf0aebd5b86afd05823
5
5
  SHA512:
6
- metadata.gz: 2c311b57edf7649f606f5d983718de9659808d13d28dba0f5d34c20e56b796296fbd702c7e58e30271c525041973c1f4fa587d5963433131b0c7d785f9374e7d
7
- data.tar.gz: ec31286dee9223c8b9ca64827b40783e97dcecc5a4ee06192b75afe125fc341a724f036851c520da4861e9dc2a03b4e7690ee672a85f27712619de210c5c1141
6
+ metadata.gz: def04235fac925e1d93a69ea6c4e1bc934aed21aea42fa2001753c1a99176801b27dbb85968958762a97ec75b764c6182ca8ac35a2d07f31ef82225750bf0a51
7
+ data.tar.gz: d547365f887e0bbceb29032b2b98cc325e226b075a900996fa9aec64248c71f165d9d5e9190af388f1f821b1a569d7fce06f261890b22cffb2f160d6c2122acd
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # FFI::LLVMJIT
2
2
 
3
- Extends Ruby FFI and uses LLVM to generate JIT wrappers for attached native functions. Works only on MRI.
3
+ Extends Ruby FFI and uses LLVM to generate JIT wrappers for attached native functions. Works only on MRI, doesn't support Windows yet.
4
4
 
5
5
  ## Requirements
6
6
 
@@ -25,9 +25,9 @@ gem install ffi-llvm-jit
25
25
 
26
26
  ## Usage
27
27
 
28
- This gem provides `FFI::LLVMJIT::Library` module that intends to be fully compatible with (`FFI::Library`)[https://www.rubydoc.info/gems/ffi/1.17.2/FFI/Library#attach_function-instance_method]. It defines its own `attach_function` method to create a faster JIT fuction instead of a FFI wrapper. The only difference for the caller is that `attach_function` returns `nil` instead of `FFI::Function`/`FFI::VariadicInvoker` when JIT function is created.
28
+ This gem provides the `FFI::LLVMJIT::Library` module that intends to be fully compatible with [FFI::Library](https://www.rubydoc.info/gems/ffi/1.17.2/FFI/Library#attach_function-instance_method). It defines its own `attach_function` method to create a faster JIT function instead of a FFI wrapper. When a JIT function is created, `attach_function` still returns an `FFI::Function` for API compatibility (though the method uses the JIT implementation). Use `attach_llvm_jit_function` if you want `nil` on success or an explicit error on failure.
29
29
 
30
- Only basic types and none configuration options are supported; in case of unsupported parameters `ffi-llvm-jit` simply calls `ffi`. It also provides `attach_llvm_jit_function` method that raises an exception instead in that case.
30
+ Supported features include basic scalar types, typedefs, enums, `FFI::DataConverter` mapped types (including stacked converters, note that it differs slightly from how FFI [behaves](https://github.com/ffi/ffi/pull/1185)), blocking calls, and errno saving. Unsupported parameters (varargs, callbacks, `:pointer` arguments) cause `attach_function` to fall back to FFI, or raise `FFI::LLVMJIT::UnsupportedError` when using `attach_llvm_jit_function`.
31
31
 
32
32
  Example:
33
33
 
@@ -39,23 +39,23 @@ module LibCFFI
39
39
  ffi_lib FFI::Library::LIBC
40
40
  end
41
41
 
42
- begin
43
- LibCFFI.attach_llvm_jit_function :printf, [:string, :varargs], :int
44
- rescue NotImplementedError => e
45
- e
46
- end
47
- # => #<NotImplementedError: Cannot create JIT function printf>
48
-
42
+ # Varargs are unsupported — attach_function falls back to FFI and returns a VariadicInvoker
49
43
  LibCFFI.attach_function :printf, [:string, :varargs], :int
50
- # => #<FFI::VariadicInvoker:0x0000766a3ac4a200 @fixed=[#<FFI::Type::Builtin::STRING size=8 alignment=8>], @type_map=nil>
44
+ # => #<FFI::VariadicInvoker:0x0000766a3ac4a200 ...>
51
45
 
46
+ # For supported types, attach_function returns FFI::Function (JIT is still used for the actual call)
47
+ LibCFFI.attach_function :strlen, [:string], :size_t
48
+ # => #<FFI::Function address=0x000070e75099d8a0>
49
+
50
+ # attach_llvm_jit_function raises FFI::LLVMJIT::UnsupportedError for unsupported types
52
51
  begin
53
- LibCFFI.attach_llvm_jit_function :strcasecmp, [:string, :string], :int, blocking: true
54
- rescue NotImplementedError => e
55
- e
52
+ LibCFFI.attach_llvm_jit_function :printf, [:string, :varargs], :int
53
+ rescue FFI::LLVMJIT::UnsupportedError => e
54
+ e.message
56
55
  end
57
- # => #<NotImplementedError: Cannot create JIT function strcasecmp>
56
+ # => "Unsupported argument type: #<FFI::Type::Builtin::VARARGS ...>"
58
57
 
58
+ # Basic function — JIT compiled, returns nil
59
59
  LibCFFI.attach_llvm_jit_function :strcasecmp, [:string, :string], :int
60
60
  # => nil
61
61
 
@@ -63,11 +63,100 @@ LibCFFI.strcasecmp('aBBa', 'AbbA')
63
63
  # => 0
64
64
  ```
65
65
 
66
+ ### Blocking calls
67
+
68
+ Pass `blocking: true` to release the GVL while the native function runs, allowing other Ruby threads to execute concurrently. Exceptions raised in other threads during the call are propagated correctly.
69
+
70
+ ```ruby
71
+ LibCFFI.attach_llvm_jit_function :sleep, [:uint], :uint, blocking: true
72
+
73
+ thread = Thread.new { LibCFFI.sleep(3600) }
74
+ sleep(0.1) until thread.stop?
75
+ thread.kill # works — GVL is released during the blocking call
76
+ ```
77
+
78
+ ### Enums
79
+
80
+ Enum symbols from `enum` declarations are resolved automatically before JIT calls. You can also pass a custom `FFI::Enums` object via the `enums:` option.
81
+
82
+ ```ruby
83
+ module LibC
84
+ extend FFI::LLVMJIT::Library
85
+ ffi_lib FFI::Library::LIBC
86
+ enum :open_flags, [:rdonly, 0, :wronly, 1, :rdwr, 2]
87
+ attach_llvm_jit_function :open, [:string, :open_flags], :int
88
+ end
89
+ LibC.open('/dev/null', :rdonly) # symbol :rdonly resolved to 0
90
+ # => 5
91
+
92
+ # Custom enums object:
93
+ enums = FFI::Enums.new
94
+ enums << FFI::Enum.new([:rdonly, 0])
95
+ LibC.attach_llvm_jit_function :open2, :open, [:string, :int], :int, enums: enums
96
+ ```
97
+
98
+ ### DataConverter
99
+
100
+ Types implementing `FFI::DataConverter` (mapped types) work for both arguments and return values. Stacked converters (where one converter's `native_type` is another `FFI::DataConverter`) are also supported.
101
+
102
+ > [!WARNING]
103
+ > Stacked converters currently [don't work](https://github.com/ffi/ffi/pull/1185) on MRI with the regular FFI gem.
104
+
105
+ ```ruby
106
+ Squared = Class.new do
107
+ extend FFI::DataConverter
108
+ native_type FFI::Type::INT
109
+ def self.to_native(value, _ctx) = value**2
110
+ def self.from_native(value, _ctx) = value * 2
111
+ end
112
+
113
+ module Lib
114
+ extend FFI::LLVMJIT::Library
115
+ # ...
116
+ attach_llvm_jit_function :abs, [Squared], Squared
117
+ end
118
+ Lib.abs(3) # to_native(3) => 9; C returns abs(9) = 9; from_native(9) => 18
119
+ ```
120
+
121
+ ### Errno
122
+
123
+ `FFI.errno` is saved after every JIT call, matching standard FFI behavior.
124
+
125
+ ```ruby
126
+ FFI.errno = 0
127
+ LibCFFI.strtol('9' * 30, nil, 10) # overflows
128
+ FFI.errno # => Errno::ERANGE::Errno
129
+ ```
130
+
131
+ ### Typedefs
132
+
133
+ Type aliases defined with `typedef` are resolved transparently by the JIT.
134
+
135
+ ```ruby
136
+ module Lib
137
+ extend FFI::LLVMJIT::Library
138
+ ffi_lib FFI::Library::LIBC
139
+ typedef :size_t, :length
140
+ attach_llvm_jit_function :strlen, [:string], :length # :length resolves to :size_t
141
+ end
142
+ ```
143
+
144
+ > [!NOTE]
145
+ > The `type_map:` option is ignored by `ffi-llvm-jit` (as it is by FFI for non-variadic functions). Use `typedef` on the module instead.
146
+
147
+ ### Fork safety
148
+
149
+ Functions attached before a fork work correctly in the child process. Because `ffi-llvm-jit` uses eager (non-lazy) LLVM compilation, the native wrapper code is fully compiled at attach time and requires no further interaction with the JIT engine in the child.
150
+
151
+ Attaching new functions after a fork is not supported — LLVM's JIT engine is not fork-safe; `attach_llvm_jit_function` raises `FFI::LLVMJIT::UnsupportedError`, and `attach_function` falls back to FFI silently.
152
+
153
+ Forking servers (Unicorn, Puma in cluster mode, etc.) work fine in practice because `attach_function` is normally called at require time, and the server forks workers only after the application is fully loaded.
154
+
66
155
  ## Benchmarks
67
156
 
68
157
  `FFI::LLVMJIT` can be up to 2x faster when used with fast native functions, where FFI overhead is especially significant.
69
158
 
70
- Below is a benchmark that compares Ruby's `bytesize` method called directly and indirectly with C `strlen` method called via LLVMJIT, C extension and FFI respectively
159
+ Below is a benchmark that compares Ruby's `bytesize` method called directly and indirectly with the C `strlen` method called via LLVMJIT, a C extension, and FFI respectively.
71
160
 
72
161
  ```
73
162
  Comparison:
@@ -82,7 +171,7 @@ Comparison:
82
171
 
83
172
  After checking out the repo, run `bin/setup` to install dependencies.
84
173
 
85
- LLVM 17 is used for development, install it via `apt install llvm17-dev` or change `ruby-llvm` version in [ffi-llvm-jit.gemspec](./ffi-llvm-jit.gemspec) if you want to use another version of LLVM.
174
+ LLVM 17 is used for development. Install it via `apt install llvm17-dev`, or change the `ruby-llvm` version in [ffi-llvm-jit.gemspec](./ffi-llvm-jit.gemspec) to use a different version of LLVM.
86
175
 
87
176
  Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
88
177
 
@@ -92,6 +181,18 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
92
181
 
93
182
  Bug reports and pull requests are welcome [on GitHub](https://github.com/uvlad7/ffi-llvm-jit).
94
183
 
184
+ ## AI assistance disclosure
185
+
186
+ The core idea behind this gem — using LLVM's JIT compiler to eliminate FFI call overhead by generating native Ruby-to-C bridge functions at runtime — as well as the entire implementation, architecture, and design decisions are the author's original work.
187
+
188
+ Claude (Anthropic) was used in an assistive capacity for:
189
+
190
+ - **Documentation** — drafting and editing README sections, including usage examples and feature descriptions.
191
+ - **Specs** — helping write RSpec test cases for newly added features.
192
+ - **API discovery** — searching LLVM C API and ruby-llvm documentation to find relevant functions and capabilities. For example, `LLVM::C.add_symbol` — which registers native symbols with the JIT engine's global symbol table before compilation — was found this way, as was `LLVM::C.search_for_address_of_symbol` used to validate that all external declarations are resolved.
193
+
194
+ All code, including the LLVM IR generation, the blocking call mechanism, the DataConverter pipeline, and the FFI compatibility layer, was written by the author without AI code generation.
195
+
95
196
  ## License
96
197
 
97
198
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -6,5 +6,6 @@ require 'mkmf'
6
6
  # with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
7
7
  # selectively, or entirely remove this flag.
8
8
  append_cflags('-fvisibility=hidden')
9
+ append_cflags('-Werror=implicit-function-declaration')
9
10
 
10
11
  create_makefile('llvm_jit/ffi_llvm_jit')
@@ -6,7 +6,7 @@ VALUE rb_mFFILLVMJITLibrary;
6
6
 
7
7
  // from https://github.com/ffi/ffi/blob/master/ext/ffi_c/Function.c
8
8
  static VALUE
9
- attach_rb_wrap_function(VALUE module, VALUE name_val, VALUE func_val, VALUE argc_val)
9
+ attach_rb_wrap_function(VALUE module, VALUE name_val, VALUE func_val, VALUE argc_val, VALUE private)
10
10
  {
11
11
  const char * name = StringValueCStr(name_val);
12
12
  VALUE (*func)(ANYARGS);
@@ -16,7 +16,7 @@ attach_rb_wrap_function(VALUE module, VALUE name_val, VALUE func_val, VALUE argc
16
16
  // rb_raise(rb_eRuntimeError, "trying to attach function to non-module");
17
17
  // return Qnil;
18
18
  // }
19
- func = (VALUE (*)(VALUE))NUM2PTR(func_val);
19
+ func = (VALUE (*)(ANYARGS))NUM2PTR(func_val);
20
20
  if (func == NULL)
21
21
  {
22
22
  rb_raise(rb_eRuntimeError, "trying to attach NULL function");
@@ -24,8 +24,13 @@ attach_rb_wrap_function(VALUE module, VALUE name_val, VALUE func_val, VALUE argc
24
24
  }
25
25
  argc = NUM2INT(argc_val);
26
26
  // rb_define_module_function uses rb_define_private_method instead of rb_define_method
27
- rb_define_singleton_method(module, name, func, argc);
28
- rb_define_method(module, name, func, argc);
27
+ if (RTEST(private)) {
28
+ rb_define_private_method(rb_singleton_class(module), name, func, argc);
29
+ rb_define_private_method(module, name, func, argc);
30
+ } else {
31
+ rb_define_singleton_method(module, name, func, argc);
32
+ rb_define_method(module, name, func, argc);
33
+ }
29
34
 
30
35
  // return self;
31
36
  return module;
@@ -37,5 +42,14 @@ Init_ffi_llvm_jit(void)
37
42
  rb_mFFI = rb_define_module("FFI");
38
43
  rb_mFFILLVMJIT = rb_define_module_under(rb_mFFI, "LLVMJIT");
39
44
  rb_mFFILLVMJITLibrary = rb_define_module_under(rb_mFFILLVMJIT, "Library");
40
- rb_define_private_method(rb_mFFILLVMJITLibrary, "attach_rb_wrap_function", attach_rb_wrap_function, 3);
45
+ rb_define_const(rb_mFFILLVMJITLibrary, "LLVM_STDCALL",
46
+ // That's how FFI hadles it, see https://github.com/ffi/ffi/blob/5b44581847bf167b83db51ac64aa409ccc9cabee/ext/ffi_c/FunctionInfo.c#L233
47
+ // the only supported calling convention other than default is stdcall on x86 windows
48
+ #if defined(X86_WIN32)
49
+ rb_intern("x86_stdcall")
50
+ #else
51
+ Qnil
52
+ #endif
53
+ );
54
+ rb_define_private_method(rb_mFFILLVMJITLibrary, "attach_rb_wrap_function", attach_rb_wrap_function, 4);
41
55
  }
@@ -11,7 +11,7 @@ RbConfig::MAKEFILE_CONFIG['LDSHARED'] =
11
11
  # RbConfig::MAKEFILE_CONFIG['DLEXT'] = RbConfig::CONFIG['DLEXT'] = 'bc'
12
12
 
13
13
  # required to push flags without checking
14
- $CFLAGS << ' -emit-llvm -c ' # rubocop:disable Style/GlobalVars
14
+ $CFLAGS << ' -emit-llvm -c -Werror=implicit-function-declaration ' # rubocop:disable Style/GlobalVars
15
15
 
16
16
  # MakeMakefile::COMPILE_C = config_string('COMPILE_C') ||
17
17
  # '$(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG) -c $(CSRCFLAG)$<'
@@ -146,10 +146,10 @@ __attribute__((always_inline)) VALUE ffi_llvm_jit_bool_to_value(bool arg) {
146
146
  // TODO: Since we generate code for every function, we could easily support safe
147
147
  // non-nullable arguments with almost no overhead.
148
148
  __attribute__((always_inline)) char * ffi_llvm_jit_value_to_string(VALUE arg) {
149
- return NIL_P(arg) ? NULL : StringValueCStr(arg);
149
+ return unlikely(NIL_P(arg)) ? NULL : StringValueCStr(arg);
150
150
  }
151
151
  __attribute__((always_inline)) VALUE ffi_llvm_jit_string_to_value(char * arg) {
152
- return arg != NULL ? rb_str_new2(arg) : Qnil;
152
+ return likely(arg != NULL) ? rb_str_new2(arg) : Qnil;
153
153
  }
154
154
  // /** The function takes a variable number of arguments */
155
155
  // NATIVE_VARARGS,
@@ -163,3 +163,35 @@ __attribute__((always_inline)) VALUE ffi_llvm_jit_string_to_value(char * arg) {
163
163
  // /** Custom native type */
164
164
  // NATIVE_MAPPED,
165
165
  // } NativeType;
166
+
167
+ __attribute__((always_inline)) void ffi_llvm_jit_rb_gc_guard(VALUE v) {
168
+ RB_GC_GUARD(v);
169
+ }
170
+
171
+ VALUE ffi_llvm_jit_save_exception(VALUE data, VALUE exc) {
172
+ VALUE* store = (VALUE *) data;
173
+ *store = exc;
174
+ return Qnil;
175
+ }
176
+
177
+ __attribute__((always_inline)) void ffi_llvm_jit_raise_exception(VALUE exc) {
178
+ // For now, RTEST isn't needed here
179
+ if (exc) {
180
+ rb_exc_raise(exc);
181
+ }
182
+ }
183
+
184
+ typedef struct
185
+ {
186
+ void* (*call_blocking_function_fn)(void *);
187
+ void *params_store;
188
+ } ffi_llvm_jit_blocking_call_t;
189
+
190
+ VALUE
191
+ ffi_llvm_jit_blocking_call(VALUE data)
192
+ {
193
+ ffi_llvm_jit_blocking_call_t* call_data = (ffi_llvm_jit_blocking_call_t *) data;
194
+ rb_thread_call_without_gvl(call_data->call_blocking_function_fn, call_data->params_store, (rb_unblock_function_t *)-1, NULL);
195
+
196
+ return Qnil;
197
+ }
@@ -2,7 +2,29 @@
2
2
  #define FFI_LLVM_JIT_LLVM_BITCODE_H 1
3
3
 
4
4
  #include "ruby.h"
5
+ #include "ruby/thread.h"
5
6
  // #include <stdint.h>
6
7
  #include <stdbool.h>
7
8
 
9
+ #ifdef __GNUC__
10
+ # define likely(x) __builtin_expect((x), 1)
11
+ # define unlikely(x) __builtin_expect((x), 0)
12
+ #else
13
+ # define likely(x) (x)
14
+ # define unlikely(x) (x)
15
+ #endif
16
+
17
+ /* Resolved at JIT load time via LLVM::C.add_symbol */
18
+ extern void ffi_llvm_jit_save_errno(void);
19
+
20
+ __attribute__((used)) static void *llvm_keepalive[] = {
21
+ (void *)ffi_llvm_jit_save_errno,
22
+ (void *)rb_thread_call_without_gvl,
23
+ (void *)rb_rescue2,
24
+ };
25
+
26
+ __attribute__((used)) static VALUE *llvm_keepalive_values[] = {
27
+ &rb_eException,
28
+ };
29
+
8
30
  #endif /* FFI_LLVM_JIT_LLVM_BITCODE_H */
@@ -2,6 +2,6 @@
2
2
 
3
3
  module FFI
4
4
  module LLVMJIT
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
data/lib/ffi/llvm_jit.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  require 'ffi'
4
6
  require 'llvm/core'
7
+ require 'llvm/linker'
5
8
  require 'llvm/execution_engine'
6
9
 
7
10
  require_relative 'llvm_jit/version'
@@ -15,6 +18,8 @@ module FFI
15
18
 
16
19
  # Ruby FFI JIT using LLVM
17
20
  module LLVMJIT
21
+ class UnsupportedError < RuntimeError; end
22
+
18
23
  # Extension to FFI::Library to support JIT compilation using LLVM
19
24
  module Library # rubocop:disable Metrics/ModuleLength
20
25
  include ::FFI::Library
@@ -22,10 +27,30 @@ module FFI
22
27
  LLVM_MOD = LLVM::Module.parse_bitcode(
23
28
  File.expand_path("llvm_jit/llvm_bitcode.#{RbConfig::MAKEFILE_CONFIG['DLEXT']}", __dir__),
24
29
  )
30
+ LLVM_MOD.verify!
31
+
32
+ # Register FFI converter addresses with LLVM's global symbol table
33
+ # before JIT engine creation so they are resolved on first compilation.
34
+ LLVM::C.add_symbol(
35
+ 'ffi_llvm_jit_save_errno',
36
+ FFI::DynamicLibrary.send(
37
+ :load_library, FFI::CURRENT_PROCESS, nil,
38
+ ).find_function('rbffi_save_errno'),
39
+ )
40
+
25
41
  LLVM.init_jit
26
42
  LLVM_ENG = LLVM::JITCompiler.new(LLVM_MOD, opt_level: 3)
43
+ LLVM_MUTEX = Mutex.new
44
+
45
+ # Validate all external declarations in the bitcode module are resolved.
46
+ # LLVM intrinsics (llvm.*) are handled natively by the JIT and not in the symbol table.
47
+ unresolved = LLVM_MOD.functions.select do |f|
48
+ f.declaration?.nonzero? && !f.name.start_with?('llvm.') &&
49
+ LLVM::C.search_for_address_of_symbol(f.name).null?
50
+ end
51
+ raise "Unresolved JIT symbols: #{unresolved.map(&:name).join(', ')}" unless unresolved.empty?
27
52
 
28
- private_constant :LLVM_MOD, :LLVM_ENG
53
+ private_constant :LLVM_MOD, :LLVM_ENG, :LLVM_MUTEX
29
54
 
30
55
  # LLVM_ENG.dispose is never called
31
56
  # https://llvm.org/doxygen/group__LLVMCTarget.html#gaaa9ce583969eb8754512e70ec4b80061
@@ -35,8 +60,12 @@ module FFI
35
60
  # bits = FFI.type_size(:int) * 8
36
61
  # ::LLVM::Int = const_get("Int#{bits}")
37
62
  # see @LLVMinst inttoptr
38
- POINTER = LLVM.const_get("Int#{FFI.type_size(:pointer) * 8}")
39
- VALUE = POINTER
63
+ INTPTR = LLVM.const_get("Int#{FFI.type_size(:pointer) * 8}")
64
+ VALUE = INTPTR
65
+ VOID_PTR_T = LLVM.Pointer() # Opaque pointer I guess
66
+ BLOCKING_CALL_T = LLVM_MOD.types['struct.ffi_llvm_jit_blocking_call_t']
67
+ raise 'BLOCKING_CALL_T not found' unless BLOCKING_CALL_T
68
+
40
69
  LLVM_TYPES = {
41
70
  # Again, not sure. Char resolves into int8, but internally it uses 'signed char'
42
71
  void: LLVM.Void,
@@ -57,11 +86,11 @@ module FFI
57
86
  # anyway, they are just aliases
58
87
  float: LLVM::Float,
59
88
  double: LLVM::Double,
60
- bool: LLVM.const_get("Int#{FFI.type_size(:bool) * 8}"),
61
- string: LLVM.Pointer(LLVM::Int8),
89
+ bool: LLVM::Int1,
90
+ string: LLVM.Pointer(LLVM.const_get("Int#{FFI.type_size(:char) * 8}")),
62
91
  }.freeze
63
92
 
64
- private_constant :POINTER, :VALUE, :LLVM_TYPES
93
+ private_constant :INTPTR, :VALUE, :VOID_PTR_T, :BLOCKING_CALL_T, :LLVM_TYPES, :LLVM_STDCALL
65
94
 
66
95
  # TODO: LLVM args
67
96
  # FFI::Type::Builtin to LLVM types
@@ -94,29 +123,40 @@ module FFI
94
123
  SUPPORTED_FROM_NATIVE.freeze
95
124
  private_constant :SUPPORTED_TO_NATIVE, :SUPPORTED_FROM_NATIVE
96
125
 
97
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
126
+ ENUM_TYPES = Set[
127
+ :int8, :int16, :int32, :uint8, :uint16, :uint32, :int64, :uint64, :long, :ulong, :float, :double, :long_double,
128
+ ].freeze
129
+ private_constant :ENUM_TYPES
130
+
131
+ INIT_PID = Process.pid
132
+ private_constant :INIT_PID
133
+
134
+ # rubocop:disable Metrics/MethodLength, Metrics/BlockLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
98
135
 
99
- # @note Return type doesn't match the original method, but it's usually not used
100
136
  # @see https://www.rubydoc.info/gems/ffi/FFI/Library#attach_function-instance_method FFI::Library.attach_function
101
137
  def attach_function(name, func, args, returns = nil, options = nil)
102
- mname, cname, arg_types, ret_type, options = convert_params(name, func, args, returns, options)
103
- return if attached_llvm_jit_function?(mname, cname, arg_types, ret_type, options)
104
-
105
- super(mname, cname, arg_types, ret_type, options)
138
+ mname, cname, arg_types, ret_type, options = convert_attach_function_params(name, func, args, returns, options)
139
+ function_handle = find_function_handle(cname, arg_types)
140
+ attach_function_handle(function_handle, mname, arg_types, ret_type, options)
106
141
  end
107
142
 
108
143
  # Same as +attach_function+, but raises an exception if cannot create JIT function
109
144
  # instead of falling back to the regular FFI function
110
145
  def attach_llvm_jit_function(name, func, args, returns = nil, options = nil)
111
- mname, cname, arg_types, ret_type, options = convert_params(name, func, args, returns, options)
112
- return if attached_llvm_jit_function?(mname, cname, arg_types, ret_type, options)
113
-
114
- raise NotImplementedError, "Cannot create JIT function #{name}"
146
+ # TODO: support LLVM call_conv; note that function_names must be patched for that
147
+ # (they also forgot an underscore on Windows for cdecl)
148
+ # https://en.wikipedia.org/wiki/Name_mangling#C
149
+ # (see core_ffi.rb and https://llvm.org/doxygen/namespacellvm_1_1CallingConv.html)
150
+ mname, cname, arg_types, ret_type, options = convert_attach_function_params(name, func, args, returns, options)
151
+ function_handle = find_function_handle(cname, arg_types)
152
+ attach_function_handle(function_handle, mname, arg_types, ret_type, options, jit_only: true)
115
153
  end
116
154
 
117
155
  private
118
156
 
119
- def convert_params(name, func, args, returns, options)
157
+ # Part copied from refactored FFI for compatibility
158
+
159
+ def convert_attach_function_params(name, func, args, returns, options)
120
160
  mname = name
121
161
  a2 = func
122
162
  a3 = args
@@ -129,6 +169,7 @@ module FFI
129
169
  end
130
170
  # Convert :foo to the native type
131
171
  arg_types = arg_types.map { |e| find_type(e) }
172
+ ret_type = find_type(ret_type)
132
173
  options = {
133
174
  convention: ffi_convention,
134
175
  type_map: defined?(@ffi_typedefs) ? @ffi_typedefs : nil,
@@ -142,92 +183,347 @@ module FFI
142
183
  [mname, cname, arg_types, ret_type, options]
143
184
  end
144
185
 
145
- def attached_llvm_jit_function?(mname, cname, arg_types, ret_type, options)
146
- # TODO: support stdcall convention (rb_func.call_conv=)
147
- # TODO: support call_without_gvl
148
- # Variadic functions are not supported; we could support known arguments,
149
- # but we'd still need to know use libffi to create varargs
150
- ret_type_name = SUPPORTED_FROM_NATIVE[find_type(ret_type)]
151
- arg_type_names = arg_types.map { |arg_type| SUPPORTED_TO_NATIVE[arg_type] }
152
- if options[:convention] != :default || !options[:type_map].nil? ||
153
- options[:blocking] || options[:enums] || ret_type_name.nil? || arg_type_names.any?(&:nil?)
154
- return false
155
- end
156
-
157
- function_handle = ffi_libraries.find do |lib|
158
- fn = nil
186
+ def find_function_handle(cname, arg_types)
187
+ ffi_libraries.each do |lib|
159
188
  begin
160
- function_names(cname, arg_types).find do |fname|
189
+ function_names(cname, arg_types).each do |fname|
161
190
  fn = lib.find_function(fname)
191
+ return fn if fn
162
192
  end
163
193
  rescue LoadError
164
194
  # Ignored
165
195
  end
166
- break fn if fn
167
196
  end
168
- raise FFI::NotFoundError.new(cname.to_s, ffi_libraries.map(&:name)) unless function_handle
169
197
 
170
- attach_llvm_jit_function_addr(mname, function_handle.address, arg_type_names, ret_type_name)
171
- # singleton_class.alias_method rb_name, jit_name
172
- # alias_method rb_name, jit_name
173
- true
198
+ raise FFI::NotFoundError.new(cname.to_s, ffi_libraries.map(&:name))
199
+ end
200
+
201
+ ###### End ######
202
+
203
+ def attach_function_handle(function_handle, mname, arg_types, ret_type, options, jit_only: false)
204
+ attach_llvm_jit_function_handle(function_handle, mname, arg_types, ret_type, options)
205
+ rescue UnsupportedError
206
+ raise if jit_only
207
+
208
+ # Part copied from refactored FFI for compatibility
209
+ invoker = if arg_types[-1] == FFI::NativeType::VARARGS
210
+ VariadicInvoker.new(function_handle, arg_types, ret_type, options)
211
+ else
212
+ Function.new(ret_type, arg_types, function_handle, options)
213
+ end
214
+ invoker.attach(self, mname.to_s)
215
+ invoker
216
+ else
217
+ return if jit_only
218
+
219
+ invoker = Function.new(ret_type, arg_types, function_handle, options)
220
+ @ffi_functions ||= {}
221
+ @ffi_functions[mname.to_s.to_sym] = invoker
222
+ invoker
223
+ end
224
+
225
+ def attach_llvm_jit_function_handle(function_handle, mname, arg_types, ret_type, options)
226
+ raise UnsupportedError, "Can't use LLVM after fork" unless Process.pid == INIT_PID
227
+
228
+ unknown_options = options.keys - %i[convention type_map blocking enums]
229
+ unless unknown_options.empty?
230
+ raise UnsupportedError, "Unsupported option#{'s' if unknown_options.size > 1}: #{unknown_options.join(', ')}"
231
+ end
232
+
233
+ type_mappers = []
234
+ arg_types = arg_types.map.with_index do |arg_type, i|
235
+ while arg_type.is_a?(Type::Mapped)
236
+ type_mappers[i] ||= []
237
+ type_mappers[i].push(arg_type)
238
+ arg_type = arg_type.native_type
239
+ end
240
+ arg_type
241
+ end
242
+
243
+ while ret_type.is_a?(Type::Mapped)
244
+ type_mappers[arg_types.size] ||= []
245
+ type_mappers[arg_types.size].unshift(ret_type)
246
+ ret_type = ret_type.native_type
247
+ end
248
+
249
+ # TODO: support call conventions other than stdcall (rb_func.call_conv=)
250
+ # TODO: support call_without_gvl
251
+ # Variadic functions are not supported; we could support known arguments,
252
+ # but we'd still need to know use libffi to create varargs
253
+ ret_type_name = SUPPORTED_FROM_NATIVE.fetch(ret_type) do
254
+ raise UnsupportedError, "Unsupported return type: #{ret_type.inspect}"
255
+ end
256
+
257
+ arg_type_names = arg_types.map do |arg_type|
258
+ SUPPORTED_TO_NATIVE.fetch(arg_type) do
259
+ raise UnsupportedError, "Unsupported argument type: #{arg_type.inspect}"
260
+ end
261
+ end
262
+ enum_types = []
263
+ unless options[:enums].nil?
264
+ arg_type_names.each_with_index { |arg_type_name, i| enum_types.push(i) if ENUM_TYPES.include?(arg_type_name) }
265
+ end
266
+ # Value type_map from opts is ignored by FFI for regular functions and is used only in Variadic
267
+ # Here we do the same and don't need to guard against type_map
268
+
269
+ call_conv = options[:convention]&.to_s == 'stdcall' ? LLVM_STDCALL : nil
270
+ rb_func_addr, uniq_id = llvm_jit_function_addr(
271
+ mname, function_handle.address, arg_type_names, ret_type_name, call_conv,
272
+ blocking: options[:blocking],
273
+ )
274
+ attach_jit_and_wrappers(mname, rb_func_addr, uniq_id, arg_types, enum_types, type_mappers, options)
275
+ end
276
+
277
+ # rubocop:disable Metrics/ParameterLists
278
+ def attach_jit_and_wrappers(mname, rb_func_addr, uniq_id, arg_types, enum_types, type_mappers, options)
279
+ if enum_types.empty? && type_mappers.empty?
280
+ attach_rb_wrap_function(mname.to_s, rb_func_addr, arg_types.size, false)
281
+ else
282
+ # mapped.to_native is the same as mapped.converter.to_native
283
+ # mapped.from_native is the same as mapped.converter.from_native
284
+ # mapped.native_type is the same as mapped.converter.native_type
285
+ enums_and_mappers = [options[:enums], type_mappers] # rubocop:disable Lint/UselessAssignment
286
+ code = <<-CODE
287
+ @_ffi_jit_enums_and_mappers_#{uniq_id} = enums_and_mappers
288
+
289
+ def self.included(base)
290
+ base.instance_variable_set(:@_ffi_jit_enums_and_mappers_#{uniq_id}, @_ffi_jit_enums_and_mappers_#{uniq_id})
291
+ super
292
+ end
293
+
294
+ def self.#{mname}(#{arg_types.size.times.map { |i| "arg_#{i}" }.join(', ')})
295
+ enums, type_mappers = @_ffi_jit_enums_and_mappers_#{uniq_id}
296
+ #{
297
+ arg_types.size.times.map do |i|
298
+ next unless type_mappers[i]
299
+
300
+ "type_mappers[#{i}].each { |mapper| arg_#{i} = mapper.to_native(arg_#{i}, nil) }"
301
+ end.join("\n")
302
+ }
303
+ #{enum_types.map { |i| "arg_#{i} = enums.__map_symbol(arg_#{i}) if arg_#{i}.is_a?(Symbol)" }.join("\n")}
304
+ res = #{mname}_#{uniq_id}(#{arg_types.size.times.map { |i| "arg_#{i}" }.join(', ')})
305
+ #{
306
+ if type_mappers[arg_types.size]
307
+ i = arg_types.size
308
+ "type_mappers[#{i}].each { |mapper| res = mapper.from_native(res, nil) }"
309
+ end
310
+ }
311
+ res
312
+ end
313
+
314
+ def #{mname}(#{arg_types.size.times.map { |i| "arg_#{i}" }.join(', ')})
315
+ enums, type_mappers = self.class.instance_variable_get(:@_ffi_jit_enums_and_mappers_#{uniq_id})
316
+ #{
317
+ arg_types.size.times.map do |i|
318
+ next unless type_mappers[i]
319
+
320
+ "type_mappers[#{i}].each { |mapper| arg_#{i} = mapper.to_native(arg_#{i}, nil) }"
321
+ end.join("\n")
322
+ }
323
+ #{enum_types.map { |i| "arg_#{i} = enums.__map_symbol(arg_#{i}) if arg_#{i}.is_a?(Symbol)" }.join("\n")}
324
+ res = #{mname}_#{uniq_id}(#{arg_types.size.times.map { |i| "arg_#{i}" }.join(', ')})
325
+ #{
326
+ if type_mappers[arg_types.size]
327
+ i = arg_types.size
328
+ "type_mappers[#{i}].each { |mapper| res = mapper.from_native(res, nil) }"
329
+ end
330
+ }
331
+ res
332
+ end
333
+ CODE
334
+ attach_rb_wrap_function("#{mname}_#{uniq_id}", rb_func_addr, arg_types.size, true)
335
+ module_eval code, __FILE__, __LINE__
336
+ end
174
337
  end
338
+ # rubocop:enable Metrics/ParameterLists
175
339
 
176
- def attach_llvm_jit_function_addr(rb_name, c_address, arg_type_names, ret_type_name)
340
+ def llvm_jit_function_addr(rb_name, c_address, arg_type_names, ret_type_name, call_conv, blocking:)
177
341
  # AFAIK name doesn't need to be unique
178
342
  llvm_mod = LLVM::Module.new('llvm_jit')
179
343
  # string -> LLVM.Pointer; size_t -> LLVM::Int64
180
- fn_type = LLVM.Function(
181
- arg_type_names.map { |arg_type| LLVM_TYPES[arg_type] },
182
- LLVM_TYPES[ret_type_name],
183
- )
184
- fn_ptr_type = LLVM.Pointer(fn_type)
344
+ arg_types = arg_type_names.map { |arg_type| LLVM_TYPES[arg_type] }
345
+ ret_type = LLVM_TYPES[ret_type_name]
346
+ func_t = LLVM.Function(arg_types, ret_type)
347
+ func_ptr_t = LLVM.Pointer(func_t)
185
348
  # Unnamed, can change '' into :"#{cname}_ptr" for debugging, but unnamed is better to prevent name clashes
186
- func_ptr = llvm_mod.globals.add(POINTER, '') do |var|
349
+ func_ptr = llvm_mod.globals.add(func_ptr_t, '') do |var|
187
350
  var.linkage = :private
188
351
  var.global_constant = true
189
352
  var.unnamed_addr = true
190
- var.initializer = POINTER.from_i(c_address)
353
+ var.initializer = INTPTR.from_i(c_address).int_to_ptr(func_ptr_t)
354
+ end
355
+ void_ret = ret_type_name == :void
356
+
357
+ if blocking
358
+ params_store_fields = [*arg_types, *(ret_type unless void_ret)]
359
+ params_store_t = LLVM.Struct(*params_store_fields) unless params_store_fields.empty?
360
+ call_blocking_func = llvm_mod.functions.add(
361
+ '', [VOID_PTR_T], VOID_PTR_T,
362
+ ) do |llvm_function, params_store|
363
+ llvm_function.basic_blocks.append('entry').build do |builder|
364
+ converted_params = arg_types.map.with_index do |t, i|
365
+ builder.load2(t, builder.gep2(params_store_t, params_store, [LLVM::Int(0), LLVM::Int(i)], ''))
366
+ end
367
+ ret = emit_cfunc_call(
368
+ builder, call_conv, converted_params, func_ptr, func_t,
369
+ )
370
+ unless void_ret
371
+ builder.store(
372
+ ret, builder.gep2(params_store_t, params_store, [LLVM::Int(0), LLVM::Int(arg_types.size)], ''),
373
+ )
374
+ end
375
+ builder.ret(VOID_PTR_T.null)
376
+ end
377
+ end
191
378
  end
192
379
 
193
- # Something is wrong in case of name collizion; and even though you can
380
+ # Something is wrong in case of name collision; and even though you can
194
381
  # update rb_func.name=, function_address is still zero
195
382
  # Upd: It happens if functions are the same even though their names are different
196
-
197
383
  rb_func = llvm_mod.functions.add(
198
- :"rb_llvm_jit_wrap_#{rb_name}_#{llvm_mod.to_ptr.address}", [VALUE] * (arg_type_names.size + 1), VALUE,
384
+ :"rb_llvm_jit_wrap_#{rb_name}_#{llvm_mod.to_ptr.address}", [VALUE] * (1 + arg_type_names.size), VALUE,
199
385
  ) do |llvm_function, _rb_self, *params|
200
- llvm_function.basic_blocks.append('entry').build do |b|
386
+ llvm_function.basic_blocks.append('entry').build do |builder|
387
+ # less readable, but easier that to position builder
388
+ # TODO: figure out builder.position stuff
389
+ if blocking
390
+ params_store = builder.alloca(params_store_t) if params_store_t
391
+ call_data = builder.alloca(BLOCKING_CALL_T)
392
+ exc_store = builder.alloca(VALUE)
393
+ end
201
394
  converted_params = arg_type_names.zip(params).map do |arg_type, param|
202
- b.call(LLVM_MOD.functions["ffi_llvm_jit_value_to_#{arg_type}"], param)
395
+ builder.call(
396
+ link_external_function(llvm_mod, "ffi_llvm_jit_value_to_#{arg_type}"),
397
+ param,
398
+ )
203
399
  end
204
-
205
- func_ptr_val = b.int2ptr(func_ptr, fn_ptr_type)
206
- res = b.call2(fn_type, b.load2(fn_ptr_type, func_ptr_val), *converted_params)
207
- b.ret(
208
- if ret_type_name == :void
209
- b.load2(VALUE, LLVM_MOD.globals['ffi_llvm_jit_Qnil'])
400
+ res = if blocking
401
+ emit_blocking_call(
402
+ builder, llvm_mod, params_store_t, exc_store, converted_params, call_blocking_func,
403
+ void_ret ? nil : ret_type, params_store, call_data,
404
+ )
405
+ else
406
+ emit_cfunc_call(
407
+ builder, call_conv, converted_params, func_ptr, func_t,
408
+ )
409
+ end
410
+ # TODO: make it optional - in orig FFI there is ignoreErrno flag that's never set
411
+ builder.call(link_external_function(llvm_mod, 'ffi_llvm_jit_save_errno'))
412
+ # In FFI it's also used to re-raise from callbacks, but here it's only for blocking calls
413
+ if blocking
414
+ builder.call(
415
+ link_external_function(llvm_mod, 'ffi_llvm_jit_raise_exception'), builder.load(exc_store),
416
+ )
417
+ end
418
+ builder.ret(
419
+ if void_ret
420
+ builder.load2(VALUE, link_external_global(llvm_mod, 'ffi_llvm_jit_Qnil'))
210
421
  else
211
- b.call(LLVM_MOD.functions["ffi_llvm_jit_#{ret_type_name}_to_value"], res)
422
+ # Note for future: in FFI struct layout redefinition doesn't change ffiParameterTypes of
423
+ # already attached functions
424
+ builder.call(
425
+ link_external_function(llvm_mod, "ffi_llvm_jit_#{ret_type_name}_to_value"),
426
+ res,
427
+ )
212
428
  end,
213
429
  )
214
430
  end
215
431
  end
216
432
 
217
- # Ruby llvm_mod object isn't kept arount and might be GCed, but
218
- # it doesn't call +dispose+ automatically, so it's ok.
219
- # Note that in function name +llvm_mod.hash+ is used and it
220
- # mustn't be reused until the module is disposed, unlike
221
- # Ruby's object_id, which may be reused and cause name clashes in some rare cases.
222
- LLVM_ENG.modules.add(llvm_mod)
223
- # rb_func.name isn't always the same as rb_name, in case of name clashes
224
- # it contains a postfix like "rb_llvm_jit_wrap_strlen.1"
225
- # https://llvm.org/doxygen/group__LLVMCExecutionEngine.html
226
- attach_rb_wrap_function(rb_name.to_s, LLVM_ENG.function_address(rb_func.name), arg_type_names.size)
227
- nil
433
+ rb_func_addr = LLVM_MUTEX.synchronize do
434
+ # TODO: investigate what's more performant: function linking or link module into
435
+ # LLVM_MOD.link_into(llvm_mod)
436
+ # rb_func.dump
437
+
438
+ # Ruby llvm_mod object isn't kept around and might be GCed, but
439
+ # it doesn't call +dispose+ automatically, so it's ok.
440
+ # Note that in function name +llvm_mod.hash+ is used and it
441
+ # mustn't be reused until the module is disposed, unlike
442
+ # Ruby's object_id, which may be reused and cause name clashes in some rare cases.
443
+ LLVM_ENG.modules.add(llvm_mod)
444
+ call_blocking_func&.verify!
445
+ rb_func.verify!
446
+ llvm_mod.verify!
447
+ # rb_func.name isn't always the same as rb_name, in case of name clashes
448
+ # it contains a postfix like "rb_llvm_jit_wrap_strlen.1"
449
+ # https://llvm.org/doxygen/group__LLVMCExecutionEngine.html
450
+ LLVM_ENG.function_address(rb_func.name)
451
+ end
452
+ # I'm not sure whether func addr can be the same in ORC JIT, but I'm pretty sure module address is unique
453
+ [rb_func_addr, llvm_mod.to_ptr.address]
454
+ end
455
+
456
+ # rubocop:disable Metrics/ParameterLists
457
+ def emit_blocking_call(
458
+ builder, llvm_mod, params_store_t, exc_store, converted_params, call_blocking_func, ret_type,
459
+ params_store, call_data
460
+ )
461
+ builder.store(
462
+ # Maybe use Qnil here? But const is probably faster
463
+ VALUE.from_i(0),
464
+ exc_store,
465
+ )
466
+ converted_params.each_with_index do |p, i|
467
+ builder.store(p, builder.gep2(params_store_t, params_store, [LLVM::Int(0), LLVM::Int(i)], ''))
468
+ end
469
+ builder.store(call_blocking_func, builder.gep2(BLOCKING_CALL_T, call_data, [LLVM::Int(0), LLVM::Int(0)], ''))
470
+ builder.store(
471
+ params_store || VOID_PTR_T.null,
472
+ builder.gep2(BLOCKING_CALL_T, call_data, [LLVM::Int(0), LLVM::Int(1)], ''),
473
+ )
474
+ builder.call(
475
+ link_external_function(llvm_mod, 'rb_rescue2'),
476
+ link_external_function(llvm_mod, 'ffi_llvm_jit_blocking_call'),
477
+ builder.ptr2int(call_data, VALUE),
478
+ link_external_function(llvm_mod, 'ffi_llvm_jit_save_exception'),
479
+ builder.ptr2int(exc_store, VALUE),
480
+ builder.load2(VALUE, link_external_global(llvm_mod, 'rb_eException')),
481
+ VALUE.from_i(0),
482
+ )
483
+ return unless ret_type
484
+
485
+ builder.load2(
486
+ ret_type,
487
+ builder.gep2(params_store_t, params_store, [LLVM::Int(0), LLVM::Int(converted_params.size)], ''),
488
+ )
489
+ end
490
+ # rubocop:enable Metrics/ParameterLists
491
+
492
+ def emit_cfunc_call(builder, call_conv, converted_params, func_ptr, func_t)
493
+ func_ptr_val = builder.load(func_ptr)
494
+ # See value.rb (Function) and builder.rb (Builder#call2)
495
+ # func_ptr_val is actually an Instruction, can't set call_conv
496
+ res = builder.call2(func_t, func_ptr_val, *converted_params)
497
+ res.call_conv = call_conv if call_conv
498
+ res
499
+ end
500
+
501
+ def link_external_function(mod, name)
502
+ unless mod.functions[name]
503
+ external_function = LLVM_MOD.functions[name]
504
+ func = mod.functions.add(name, external_function.function_type)
505
+ func.linkage = :external
506
+ func.call_conv = external_function.call_conv
507
+ external_function.function_attributes.to_a.each { |attr| func.add_attribute(attr, -1) }
508
+ external_function.return_attributes.to_a.each { |attr| func.add_attribute(attr, 0) }
509
+ external_function.params.size.times do |idx|
510
+ external_function.param_attributes(idx + 1).to_a.each do |attr|
511
+ func.add_attribute(attr, idx + 1)
512
+ end
513
+ end
514
+ end
515
+ mod.functions[name]
516
+ end
517
+
518
+ def link_external_global(mod, name)
519
+ unless mod.globals[name]
520
+ glob = mod.globals.add(LLVM::Type.from_ptr(LLVM::C.get_value_type(LLVM_MOD.globals[name]), nil), name)
521
+ glob.linkage = :external
522
+ end
523
+ mod.globals[name]
228
524
  end
229
525
 
230
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
526
+ # rubocop:enable Metrics/MethodLength, Metrics/BlockLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
231
527
  end
232
528
  end
233
529
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffi-llvm-jit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - uvlad7
@@ -217,9 +217,9 @@ licenses:
217
217
  - MIT
218
218
  metadata:
219
219
  homepage_uri: https://github.com/uvlad7/ffi-llvm-jit
220
- source_code_uri: https://github.com/uvlad7/ffi-llvm-jit/tree/v0.1.0
220
+ source_code_uri: https://github.com/uvlad7/ffi-llvm-jit/tree/v0.2.0
221
221
  changelog_uri: https://github.com/uvlad7/ffi-llvm-jit/blob/main/CHANGELOG.md
222
- documentation_uri: https://rubydoc.info/gems/ffi-llvm-jit/0.1.0
222
+ documentation_uri: https://rubydoc.info/gems/ffi-llvm-jit/0.2.0
223
223
  rdoc_options: []
224
224
  require_paths:
225
225
  - lib