ffi-llvm-jit 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7330fb0bd311b6b6655240b9801e1af1951964f57210540c6d00db78aeddf001
4
+ data.tar.gz: a98d9784adebf9e4c4ecdbeb778a48ffd2a19161713f5f8a48c5dc3b00abac58
5
+ SHA512:
6
+ metadata.gz: 2c311b57edf7649f606f5d983718de9659808d13d28dba0f5d34c20e56b796296fbd702c7e58e30271c525041973c1f4fa587d5963433131b0c7d785f9374e7d
7
+ data.tar.gz: ec31286dee9223c8b9ca64827b40783e97dcecc5a4ee06192b75afe125fc341a724f036851c520da4861e9dc2a03b4e7690ee672a85f27712619de210c5c1141
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 uvlad7
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # FFI::LLVMJIT
2
+
3
+ Extends Ruby FFI and uses LLVM to generate JIT wrappers for attached native functions. Works only on MRI.
4
+
5
+ ## Requirements
6
+
7
+ The gem depends on `ruby-llvm` gem, which requires `llvm` development package to be installed.
8
+
9
+ On Debian/Ubuntu you can install it with `apt install llvmXX-dev`, where `XX` is a major version of `ruby-llvm` gem.
10
+ For other systems, refer to `ruby-llvm` [README](https://github.com/ruby-llvm/ruby-llvm/blob/master/README.md).
11
+
12
+ ## Installation
13
+
14
+ Install the gem and add to the application's Gemfile by executing:
15
+
16
+ ```bash
17
+ bundle add ffi-llvm-jit
18
+ ```
19
+
20
+ If bundler is not being used to manage dependencies, install the gem by executing:
21
+
22
+ ```bash
23
+ gem install ffi-llvm-jit
24
+ ```
25
+
26
+ ## Usage
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.
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.
31
+
32
+ Example:
33
+
34
+ ```ruby
35
+ require 'ffi/llvm_jit'
36
+
37
+ module LibCFFI
38
+ extend FFI::LLVMJIT::Library
39
+ ffi_lib FFI::Library::LIBC
40
+ end
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
+
49
+ LibCFFI.attach_function :printf, [:string, :varargs], :int
50
+ # => #<FFI::VariadicInvoker:0x0000766a3ac4a200 @fixed=[#<FFI::Type::Builtin::STRING size=8 alignment=8>], @type_map=nil>
51
+
52
+ begin
53
+ LibCFFI.attach_llvm_jit_function :strcasecmp, [:string, :string], :int, blocking: true
54
+ rescue NotImplementedError => e
55
+ e
56
+ end
57
+ # => #<NotImplementedError: Cannot create JIT function strcasecmp>
58
+
59
+ LibCFFI.attach_llvm_jit_function :strcasecmp, [:string, :string], :int
60
+ # => nil
61
+
62
+ LibCFFI.strcasecmp('aBBa', 'AbbA')
63
+ # => 0
64
+ ```
65
+
66
+ ## Benchmarks
67
+
68
+ `FFI::LLVMJIT` can be up to 2x faster when used with fast native functions, where FFI overhead is especially significant.
69
+
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
71
+
72
+ ```
73
+ Comparison:
74
+ ruby-direct: 15089241.4 i/s
75
+ strlen-ruby: 11353201.8 i/s - 1.33x slower
76
+ strlen-ffi-llvm-jit: 10839778.2 i/s - 1.39x slower
77
+ strlen-cext: 10822451.7 i/s - 1.39x slower
78
+ strlen-ffi: 5058105.5 i/s - 2.98x slower
79
+ ```
80
+
81
+ ## Development
82
+
83
+ After checking out the repo, run `bin/setup` to install dependencies.
84
+
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.
86
+
87
+ 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
+
89
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
90
+
91
+ ## Contributing
92
+
93
+ Bug reports and pull requests are welcome [on GitHub](https://github.com/uvlad7/ffi-llvm-jit).
94
+
95
+ ## License
96
+
97
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ # Makes all symbols private by default to avoid unintended conflict
6
+ # with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
7
+ # selectively, or entirely remove this flag.
8
+ append_cflags('-fvisibility=hidden')
9
+
10
+ create_makefile('llvm_jit/ffi_llvm_jit')
@@ -0,0 +1,41 @@
1
+ #include "ffi_llvm_jit.h"
2
+
3
+ VALUE rb_mFFI;
4
+ VALUE rb_mFFILLVMJIT;
5
+ VALUE rb_mFFILLVMJITLibrary;
6
+
7
+ // from https://github.com/ffi/ffi/blob/master/ext/ffi_c/Function.c
8
+ static VALUE
9
+ attach_rb_wrap_function(VALUE module, VALUE name_val, VALUE func_val, VALUE argc_val)
10
+ {
11
+ const char * name = StringValueCStr(name_val);
12
+ VALUE (*func)(ANYARGS);
13
+ int argc;
14
+ // if (!rb_obj_is_kind_of(module, rb_cModule))
15
+ // {
16
+ // rb_raise(rb_eRuntimeError, "trying to attach function to non-module");
17
+ // return Qnil;
18
+ // }
19
+ func = (VALUE (*)(VALUE))NUM2PTR(func_val);
20
+ if (func == NULL)
21
+ {
22
+ rb_raise(rb_eRuntimeError, "trying to attach NULL function");
23
+ return Qnil;
24
+ }
25
+ argc = NUM2INT(argc_val);
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);
29
+
30
+ // return self;
31
+ return module;
32
+ }
33
+
34
+ RUBY_FUNC_EXPORTED void
35
+ Init_ffi_llvm_jit(void)
36
+ {
37
+ rb_mFFI = rb_define_module("FFI");
38
+ rb_mFFILLVMJIT = rb_define_module_under(rb_mFFI, "LLVMJIT");
39
+ 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);
41
+ }
@@ -0,0 +1,16 @@
1
+ #ifndef FFI_LLVM_JIT_H
2
+ #define FFI_LLVM_JIT_H 1
3
+
4
+ #include "ruby.h"
5
+
6
+
7
+ #if SIZEOF_VOIDP == SIZEOF_LONG
8
+ # define PTR2NUM(x) (LONG2NUM((long)(x)))
9
+ # define NUM2PTR(x) ((void*)(NUM2ULONG(x)))
10
+ #else
11
+ /* # error --->> Ruby/DL2 requires sizeof(void*) == sizeof(long) to be compiled. <<--- */
12
+ # define PTR2NUM(x) (LL2NUM((LONG_LONG)(x)))
13
+ # define NUM2PTR(x) ((void*)(NUM2ULL(x)))
14
+ #endif
15
+
16
+ #endif /* FFI_LLVM_JIT_H */
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ RbConfig::MAKEFILE_CONFIG['CC'] = RbConfig::CONFIG['CC'] = 'clang'
6
+ RbConfig::MAKEFILE_CONFIG['CXX'] = RbConfig::CONFIG['CXX'] = 'clang++'
7
+ RbConfig::MAKEFILE_CONFIG['LDSHARED'] =
8
+ RbConfig::CONFIG['LDSHARED'] = "ruby -rfileutils -e 'FileUtils.cp(ARGV[2], ARGV[1])' -- "
9
+ # RbConfig::MAKEFILE_CONFIG['MKMF_VERBOSE'] = RbConfig::CONFIG['MKMF_VERBOSE'] = '1'
10
+ # cp into lib dir won't work; just use MAKEFILE_CONFIG later to find the extname
11
+ # RbConfig::MAKEFILE_CONFIG['DLEXT'] = RbConfig::CONFIG['DLEXT'] = 'bc'
12
+
13
+ # required to push flags without checking
14
+ $CFLAGS << ' -emit-llvm -c ' # rubocop:disable Style/GlobalVars
15
+
16
+ # MakeMakefile::COMPILE_C = config_string('COMPILE_C') ||
17
+ # '$(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG) -c $(CSRCFLAG)$<'
18
+
19
+ create_makefile('llvm_jit/llvm_bitcode')
@@ -0,0 +1,165 @@
1
+ #include "llvm_bitcode.h"
2
+
3
+ // See https://github.com/ffi/ffi/blob/master/ext/ffi_c/Call.c
4
+ // https://github.com/ffi/ffi/blob/master/ext/ffi_c/Function.c
5
+ // rbffi_SetupCallParams
6
+ // and
7
+ // See https://github.com/ffi/ffi/blob/master/ext/ffi_c/Types.c
8
+ // rbffi_NativeValue_ToRuby
9
+ // typedef union {
10
+ // #ifdef USE_RAW
11
+ // signed int s8, s16, s32;
12
+ // unsigned int u8, u16, u32;
13
+ // #else
14
+ // signed char s8;
15
+ // unsigned char u8;
16
+ // signed short s16;
17
+ // unsigned short u16;
18
+ // signed int s32;
19
+ // unsigned int u32;
20
+ // #endif
21
+ // signed long long i64;
22
+ // unsigned long long u64;
23
+ // signed long sl;
24
+ // unsigned long ul;
25
+ // void* ptr;
26
+ // float f32;
27
+ // double f64;
28
+ // long double ld;
29
+ // } FFIStorage;
30
+ // typedef enum {
31
+ // NATIVE_VOID,
32
+ VALUE ffi_llvm_jit_Qnil = Qnil;
33
+ // NATIVE_INT8,
34
+ __attribute__((always_inline)) signed char ffi_llvm_jit_value_to_int8(VALUE arg) {
35
+ return NUM2INT(arg);
36
+ }
37
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_int8_to_value(signed char arg) {
38
+ return INT2NUM(arg);
39
+ }
40
+ // NATIVE_UINT8,
41
+ __attribute__((always_inline)) unsigned char ffi_llvm_jit_value_to_uint8(VALUE arg) {
42
+ return NUM2UINT(arg);
43
+ }
44
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_uint8_to_value(unsigned char arg) {
45
+ return UINT2NUM(arg);
46
+ }
47
+ // NATIVE_INT16,
48
+ __attribute__((always_inline)) signed short ffi_llvm_jit_value_to_int16(VALUE arg) {
49
+ return NUM2INT(arg);
50
+ }
51
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_int16_to_value(signed short arg) {
52
+ return INT2NUM(arg);
53
+ }
54
+ // NATIVE_UINT16,
55
+ __attribute__((always_inline)) unsigned short ffi_llvm_jit_value_to_uint16(VALUE arg) {
56
+ return NUM2UINT(arg);
57
+ }
58
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_uint16_to_value(unsigned short arg) {
59
+ return UINT2NUM(arg);
60
+ }
61
+ // NATIVE_INT32,
62
+ __attribute__((always_inline)) signed int ffi_llvm_jit_value_to_int32(VALUE arg) {
63
+ return NUM2INT(arg);
64
+ }
65
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_int32_to_value(signed int arg) {
66
+ return INT2NUM(arg);
67
+ }
68
+ // NATIVE_UINT32,
69
+ __attribute__((always_inline)) unsigned int ffi_llvm_jit_value_to_uint32(VALUE arg) {
70
+ return NUM2UINT(arg);
71
+ }
72
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_uint32_to_value(unsigned int arg) {
73
+ return UINT2NUM(arg);
74
+ }
75
+ // NATIVE_INT64,
76
+ __attribute__((always_inline)) signed long long ffi_llvm_jit_value_to_int64(VALUE arg) {
77
+ return NUM2LL(arg);
78
+ }
79
+ // TODO: Ruby defines long long differently, see include/ruby/backward/2/long_long.h
80
+ // but FFI simply uses `long long`, and so do I
81
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_int64_to_value(signed long long arg) {
82
+ return LL2NUM(arg);
83
+ }
84
+ // NATIVE_UINT64,
85
+ __attribute__((always_inline)) unsigned long long ffi_llvm_jit_value_to_uint64(VALUE arg) {
86
+ return NUM2ULL(arg);
87
+ }
88
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_uint64_to_value(unsigned long long arg) {
89
+ return ULL2NUM(arg);
90
+ }
91
+ // NATIVE_LONG,
92
+ __attribute__((always_inline)) signed long ffi_llvm_jit_value_to_long(VALUE arg) {
93
+ return NUM2LONG(arg);
94
+ }
95
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_long_to_value(signed long arg) {
96
+ return LONG2NUM(arg);
97
+ }
98
+ // NATIVE_ULONG,
99
+ __attribute__((always_inline)) unsigned long ffi_llvm_jit_value_to_ulong(VALUE arg) {
100
+ return NUM2ULONG(arg);
101
+ }
102
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_ulong_to_value(unsigned long arg) {
103
+ return ULONG2NUM(arg);
104
+ }
105
+ // NATIVE_FLOAT32,
106
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_float_to_value(float arg) {
107
+ // FFI uses rb_float_new, I prefer DBL2NUM - which is defined exactly like that - for consistency
108
+ return DBL2NUM(arg);
109
+ }
110
+ __attribute__((always_inline)) float ffi_llvm_jit_value_to_float(VALUE arg) {
111
+ return (float) NUM2DBL(arg);
112
+ }
113
+ // NATIVE_FLOAT64,
114
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_double_to_value(double arg) {
115
+ return DBL2NUM(arg);
116
+ }
117
+ __attribute__((always_inline)) double ffi_llvm_jit_value_to_double(VALUE arg) {
118
+ return NUM2DBL(arg);
119
+ }
120
+ // NATIVE_LONGDOUBLE,
121
+ // NATIVE_POINTER,
122
+ // NATIVE_FUNCTION,
123
+ // NATIVE_BUFFER_IN,
124
+ // NATIVE_BUFFER_OUT,
125
+ // NATIVE_BUFFER_INOUT,
126
+ // NATIVE_BOOL,
127
+ // They use signed char as return value, but unsigned char as param when convert into Ruby, for some reason
128
+ __attribute__((always_inline)) bool ffi_llvm_jit_value_to_bool(VALUE arg) {
129
+ // return RTEST(arg);
130
+ // I'd use RTEST, but FFI enforces that the argument is a boolean.
131
+ switch (TYPE(arg)) {
132
+ case T_TRUE:
133
+ return true;
134
+ case T_FALSE:
135
+ return false;
136
+ default:
137
+ rb_raise(rb_eTypeError, "wrong argument type (expected a boolean parameter)");
138
+ }
139
+ }
140
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_bool_to_value(bool arg) {
141
+ return arg ? Qtrue : Qfalse;
142
+ }
143
+ // /** An immutable string. Nul terminated, but only copies in to the native function */
144
+ // NATIVE_STRING,
145
+ //
146
+ // TODO: Since we generate code for every function, we could easily support safe
147
+ // non-nullable arguments with almost no overhead.
148
+ __attribute__((always_inline)) char * ffi_llvm_jit_value_to_string(VALUE arg) {
149
+ return NIL_P(arg) ? NULL : StringValueCStr(arg);
150
+ }
151
+ __attribute__((always_inline)) VALUE ffi_llvm_jit_string_to_value(char * arg) {
152
+ return arg != NULL ? rb_str_new2(arg) : Qnil;
153
+ }
154
+ // /** The function takes a variable number of arguments */
155
+ // NATIVE_VARARGS,
156
+
157
+ // /** Struct-by-value param or result */
158
+ // NATIVE_STRUCT,
159
+
160
+ // /** An array type definition */
161
+ // NATIVE_ARRAY,
162
+
163
+ // /** Custom native type */
164
+ // NATIVE_MAPPED,
165
+ // } NativeType;
@@ -0,0 +1,8 @@
1
+ #ifndef FFI_LLVM_JIT_LLVM_BITCODE_H
2
+ #define FFI_LLVM_JIT_LLVM_BITCODE_H 1
3
+
4
+ #include "ruby.h"
5
+ // #include <stdint.h>
6
+ #include <stdbool.h>
7
+
8
+ #endif /* FFI_LLVM_JIT_LLVM_BITCODE_H */
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FFI
4
+ module LLVMJIT
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require 'llvm/core'
5
+ require 'llvm/execution_engine'
6
+
7
+ require_relative 'llvm_jit/version'
8
+ require_relative 'llvm_jit/ffi_llvm_jit'
9
+
10
+ module FFI
11
+ # https://llvm.org/doxygen/group__LLVMCCoreModule.html
12
+ # https://llvm.org/doxygen/group__LLVMCBitReader.html
13
+ # https://llvm.org/doxygen/group__LLVMCCoreMemoryBuffers.html
14
+ # see llvm/core/bitcode.rb
15
+
16
+ # Ruby FFI JIT using LLVM
17
+ module LLVMJIT
18
+ # Extension to FFI::Library to support JIT compilation using LLVM
19
+ module Library # rubocop:disable Metrics/ModuleLength
20
+ include ::FFI::Library
21
+
22
+ LLVM_MOD = LLVM::Module.parse_bitcode(
23
+ File.expand_path("llvm_jit/llvm_bitcode.#{RbConfig::MAKEFILE_CONFIG['DLEXT']}", __dir__),
24
+ )
25
+ LLVM.init_jit
26
+ LLVM_ENG = LLVM::JITCompiler.new(LLVM_MOD, opt_level: 3)
27
+
28
+ private_constant :LLVM_MOD, :LLVM_ENG
29
+
30
+ # LLVM_ENG.dispose is never called
31
+ # https://llvm.org/doxygen/group__LLVMCTarget.html#gaaa9ce583969eb8754512e70ec4b80061
32
+ # LLVM_MOD.dump
33
+
34
+ # # Native integer type
35
+ # bits = FFI.type_size(:int) * 8
36
+ # ::LLVM::Int = const_get("Int#{bits}")
37
+ # see @LLVMinst inttoptr
38
+ POINTER = LLVM.const_get("Int#{FFI.type_size(:pointer) * 8}")
39
+ VALUE = POINTER
40
+ LLVM_TYPES = {
41
+ # Again, not sure. Char resolves into int8, but internally it uses 'signed char'
42
+ void: LLVM.Void,
43
+ int8: LLVM.const_get("Int#{FFI.type_size(:int8) * 8}"),
44
+ uint8: LLVM.const_get("Int#{FFI.type_size(:uint8) * 8}"),
45
+ int16: LLVM.const_get("Int#{FFI.type_size(:int16) * 8}"),
46
+ uint16: LLVM.const_get("Int#{FFI.type_size(:uint16) * 8}"),
47
+ int32: LLVM.const_get("Int#{FFI.type_size(:int32) * 8}"),
48
+ uint32: LLVM.const_get("Int#{FFI.type_size(:uint32) * 8}"),
49
+ int64: LLVM.const_get("Int#{FFI.type_size(:int64) * 8}"),
50
+ uint64: LLVM.const_get("Int#{FFI.type_size(:uint64) * 8}"),
51
+ long: LLVM.const_get("Int#{FFI.type_size(:long) * 8}"),
52
+ ulong: LLVM.const_get("Int#{FFI.type_size(:ulong) * 8}"),
53
+ # These types are actually defined as float and double in FFI
54
+ # and despite they are called float32 and float64 in the definitions
55
+ # and having FFI::NativeType::FLOAT32/FFI::NativeType::FLOAT64 constants,
56
+ # you can't find them through FFI.find_type and therefore use in attach_function
57
+ # anyway, they are just aliases
58
+ float: LLVM::Float,
59
+ double: LLVM::Double,
60
+ bool: LLVM.const_get("Int#{FFI.type_size(:bool) * 8}"),
61
+ string: LLVM.Pointer(LLVM::Int8),
62
+ }.freeze
63
+
64
+ private_constant :POINTER, :VALUE, :LLVM_TYPES
65
+
66
+ # TODO: LLVM args
67
+ # FFI::Type::Builtin to LLVM types
68
+ # FFI::NativeType.constants
69
+ # https://github.com/ffi/ffi/blob/master/ext/ffi_c/Type.c#L410
70
+
71
+ # rubocop:disable Style/MutableConstant
72
+ # Frozen later
73
+
74
+ SUPPORTED_TO_NATIVE = {}
75
+ SUPPORTED_FROM_NATIVE = {}
76
+
77
+ # rubocop:enable Style/MutableConstant
78
+
79
+ LLVM_MOD.functions.each do |func|
80
+ name = func.name
81
+ if name[/\Affi_llvm_jit_value_to_(.*)\z/, 1]
82
+ type = Regexp.last_match(1).to_sym
83
+ SUPPORTED_TO_NATIVE[FFI.find_type(type)] = type
84
+ elsif name[/\Affi_llvm_jit_(.*)_to_value\z/]
85
+ type = Regexp.last_match(1).to_sym
86
+ SUPPORTED_FROM_NATIVE[FFI.find_type(type)] = type
87
+ end
88
+
89
+ raise "Conversion function #{name} defined, but LLVM type #{type} is unknown" if type && !LLVM_TYPES.key?(type)
90
+ end
91
+
92
+ SUPPORTED_FROM_NATIVE[FFI.find_type(:void)] = :void
93
+ SUPPORTED_TO_NATIVE.freeze
94
+ SUPPORTED_FROM_NATIVE.freeze
95
+ private_constant :SUPPORTED_TO_NATIVE, :SUPPORTED_FROM_NATIVE
96
+
97
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
98
+
99
+ # @note Return type doesn't match the original method, but it's usually not used
100
+ # @see https://www.rubydoc.info/gems/ffi/FFI/Library#attach_function-instance_method FFI::Library.attach_function
101
+ 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)
106
+ end
107
+
108
+ # Same as +attach_function+, but raises an exception if cannot create JIT function
109
+ # instead of falling back to the regular FFI function
110
+ 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}"
115
+ end
116
+
117
+ private
118
+
119
+ def convert_params(name, func, args, returns, options)
120
+ mname = name
121
+ a2 = func
122
+ a3 = args
123
+ a4 = returns
124
+ a5 = options
125
+ cname, arg_types, ret_type, opts = if a4 && (a2.is_a?(String) || a2.is_a?(Symbol))
126
+ [a2, a3, a4, a5]
127
+ else
128
+ [mname.to_s, a2, a3, a4]
129
+ end
130
+ # Convert :foo to the native type
131
+ arg_types = arg_types.map { |e| find_type(e) }
132
+ options = {
133
+ convention: ffi_convention,
134
+ type_map: defined?(@ffi_typedefs) ? @ffi_typedefs : nil,
135
+ blocking: defined?(@blocking) && @blocking,
136
+ enums: defined?(@ffi_enums) ? @ffi_enums : nil,
137
+ }
138
+
139
+ @blocking = false
140
+ options.merge!(opts) if opts.is_a?(Hash)
141
+
142
+ [mname, cname, arg_types, ret_type, options]
143
+ end
144
+
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
159
+ begin
160
+ function_names(cname, arg_types).find do |fname|
161
+ fn = lib.find_function(fname)
162
+ end
163
+ rescue LoadError
164
+ # Ignored
165
+ end
166
+ break fn if fn
167
+ end
168
+ raise FFI::NotFoundError.new(cname.to_s, ffi_libraries.map(&:name)) unless function_handle
169
+
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
174
+ end
175
+
176
+ def attach_llvm_jit_function_addr(rb_name, c_address, arg_type_names, ret_type_name)
177
+ # AFAIK name doesn't need to be unique
178
+ llvm_mod = LLVM::Module.new('llvm_jit')
179
+ # 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)
185
+ # 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|
187
+ var.linkage = :private
188
+ var.global_constant = true
189
+ var.unnamed_addr = true
190
+ var.initializer = POINTER.from_i(c_address)
191
+ end
192
+
193
+ # Something is wrong in case of name collizion; and even though you can
194
+ # update rb_func.name=, function_address is still zero
195
+ # Upd: It happens if functions are the same even though their names are different
196
+
197
+ 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,
199
+ ) do |llvm_function, _rb_self, *params|
200
+ llvm_function.basic_blocks.append('entry').build do |b|
201
+ 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)
203
+ 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'])
210
+ else
211
+ b.call(LLVM_MOD.functions["ffi_llvm_jit_#{ret_type_name}_to_value"], res)
212
+ end,
213
+ )
214
+ end
215
+ end
216
+
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
228
+ end
229
+
230
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,6 @@
1
+ module FFI
2
+ module LLVMJIT
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,241 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffi-llvm-jit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - uvlad7
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.15'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ruby-llvm
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '14'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '14'
40
+ - !ruby/object:Gem::Dependency
41
+ name: pry
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 0.14.2
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 0.14.2
54
+ - !ruby/object:Gem::Dependency
55
+ name: pry-byebug
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - '='
59
+ - !ruby/object:Gem::Version
60
+ version: 3.10.1
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '='
66
+ - !ruby/object:Gem::Version
67
+ version: 3.10.1
68
+ - !ruby/object:Gem::Dependency
69
+ name: benchmark-ips
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.14'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.14'
82
+ - !ruby/object:Gem::Dependency
83
+ name: strlen
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: ruby-llvm
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '17'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '17'
110
+ - !ruby/object:Gem::Dependency
111
+ name: ffi-compiler
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.3'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.3'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rake
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '13.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '13.0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rake-compiler
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: rspec
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '3.0'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '3.0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: rubocop
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '1.21'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '1.21'
180
+ - !ruby/object:Gem::Dependency
181
+ name: yard
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: 0.9.37
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: 0.9.37
194
+ description: Extends Ruby FFI and uses LLVM to generate JIT wrappers for attached
195
+ native functions. Works only on MRI
196
+ email:
197
+ - uvlad7@gmail.com
198
+ executables: []
199
+ extensions:
200
+ - ext/ffi_llvm_jit/extconf.rb
201
+ - ext/llvm_bitcode/extconf.rb
202
+ extra_rdoc_files: []
203
+ files:
204
+ - LICENSE.txt
205
+ - README.md
206
+ - ext/ffi_llvm_jit/extconf.rb
207
+ - ext/ffi_llvm_jit/ffi_llvm_jit.c
208
+ - ext/ffi_llvm_jit/ffi_llvm_jit.h
209
+ - ext/llvm_bitcode/extconf.rb
210
+ - ext/llvm_bitcode/llvm_bitcode.c
211
+ - ext/llvm_bitcode/llvm_bitcode.h
212
+ - lib/ffi/llvm_jit.rb
213
+ - lib/ffi/llvm_jit/version.rb
214
+ - sig/ffi_llvm_jit.rbs
215
+ homepage: https://github.com/uvlad7/ffi-llvm-jit
216
+ licenses:
217
+ - MIT
218
+ metadata:
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
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
223
+ rdoc_options: []
224
+ require_paths:
225
+ - lib
226
+ required_ruby_version: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: 2.3.8
231
+ required_rubygems_version: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: 3.2.3
236
+ requirements:
237
+ - llvm-14-dev or newer
238
+ rubygems_version: 3.6.9
239
+ specification_version: 4
240
+ summary: Ruby FFI JIT using LLVM
241
+ test_files: []