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 +4 -4
- data/README.md +118 -17
- data/ext/ffi_llvm_jit/extconf.rb +1 -0
- data/ext/ffi_llvm_jit/ffi_llvm_jit.c +19 -5
- data/ext/llvm_bitcode/extconf.rb +1 -1
- data/ext/llvm_bitcode/llvm_bitcode.c +34 -2
- data/ext/llvm_bitcode/llvm_bitcode.h +22 -0
- data/lib/ffi/llvm_jit/version.rb +1 -1
- data/lib/ffi/llvm_jit.rb +366 -70
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfb626cce651accf4c6a89183521b2466b28896d51da8618e9bd5ba6d4cab0ea
|
|
4
|
+
data.tar.gz: f981dac4d5b6b28ca8deda9f8d83fac86fdba9f652341cf0aebd5b86afd05823
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 :
|
|
54
|
-
rescue
|
|
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
|
-
# =>
|
|
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
|
|
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).
|
data/ext/ffi_llvm_jit/extconf.rb
CHANGED
|
@@ -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 (*)(
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
}
|
data/ext/llvm_bitcode/extconf.rb
CHANGED
|
@@ -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 */
|
data/lib/ffi/llvm_jit/version.rb
CHANGED
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
|
-
|
|
39
|
-
VALUE =
|
|
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
|
|
61
|
-
string: LLVM.Pointer(LLVM
|
|
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 :
|
|
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
|
-
|
|
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 =
|
|
103
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
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).
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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 |
|
|
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
|
-
|
|
395
|
+
builder.call(
|
|
396
|
+
link_external_function(llvm_mod, "ffi_llvm_jit_value_to_#{arg_type}"),
|
|
397
|
+
param,
|
|
398
|
+
)
|
|
203
399
|
end
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
222
|
+
documentation_uri: https://rubydoc.info/gems/ffi-llvm-jit/0.2.0
|
|
223
223
|
rdoc_options: []
|
|
224
224
|
require_paths:
|
|
225
225
|
- lib
|