Skip to content

Commit 31684e3

Browse files
committed
wip
1 parent e318316 commit 31684e3

6 files changed

Lines changed: 758 additions & 0 deletions

File tree

include/ruby/jit_native.h

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#ifndef RUBY_JIT_NATIVE_H
2+
#define RUBY_JIT_NATIVE_H 1
3+
4+
/**
5+
* Native call hints for JIT optimization of FFI-style calls.
6+
*
7+
* C extensions that bind native functions (e.g. the FFI gem, Fiddle) can
8+
* register hints telling the JIT compiler how to call through to the actual
9+
* native function directly, bypassing the extension's generic dispatch layer
10+
* and libffi.
11+
*
12+
* Usage from a C extension:
13+
*
14+
* void *func = dlsym(handle, "my_func");
15+
* // my_func(int, const char*) -> int
16+
* int arg_types[] = { RB_JIT_NATIVE_INT, RB_JIT_NATIVE_STRING };
17+
* rb_jit_hint_native_call(klass, rb_intern("my_func"), func,
18+
* 2, arg_types, RB_JIT_NATIVE_INT);
19+
*/
20+
21+
#include "ruby/ruby.h"
22+
23+
RUBY_SYMBOL_EXPORT_BEGIN
24+
25+
enum rb_jit_native_type {
26+
RB_JIT_NATIVE_VOID = 0,
27+
RB_JIT_NATIVE_BOOL,
28+
RB_JIT_NATIVE_INT, /* C int */
29+
RB_JIT_NATIVE_UINT, /* C unsigned int */
30+
RB_JIT_NATIVE_LONG, /* C long */
31+
RB_JIT_NATIVE_ULONG, /* C unsigned long */
32+
RB_JIT_NATIVE_INT64, /* int64_t / long long */
33+
RB_JIT_NATIVE_UINT64, /* uint64_t / unsigned long long */
34+
RB_JIT_NATIVE_FLOAT, /* C float */
35+
RB_JIT_NATIVE_DOUBLE, /* C double */
36+
RB_JIT_NATIVE_POINTER, /* void * */
37+
RB_JIT_NATIVE_STRING, /* const char * (null-terminated) */
38+
};
39+
40+
#define RB_JIT_NATIVE_MAX_ARGS 6
41+
42+
typedef struct rb_jit_native_call_hint {
43+
void *func_ptr;
44+
int argc;
45+
int arg_types[RB_JIT_NATIVE_MAX_ARGS]; /* enum rb_jit_native_type */
46+
int ret_type; /* enum rb_jit_native_type */
47+
} rb_jit_native_call_hint_t;
48+
49+
/**
50+
* Register a native call hint for a method.
51+
*
52+
* Tells the JIT that +mid+ on +klass+ is a thin wrapper around +func_ptr+
53+
* with the given signature. When the JIT compiles a call to this method it
54+
* may emit a direct native call instead of going through the extension's
55+
* dispatch function and libffi.
56+
*
57+
* @param klass The class or module the method is defined on.
58+
* For singleton methods use rb_singleton_class(obj).
59+
* @param mid The method ID (rb_intern("name")).
60+
* @param func_ptr The native function pointer (e.g. from dlsym).
61+
* @param argc Number of arguments (0..RB_JIT_NATIVE_MAX_ARGS).
62+
* @param arg_types Array of argc rb_jit_native_type values.
63+
* @param ret_type Return type (rb_jit_native_type).
64+
*/
65+
void rb_jit_hint_native_call(VALUE klass, ID mid, void *func_ptr,
66+
int argc, const int *arg_types, int ret_type);
67+
68+
/**
69+
* Look up a previously registered native call hint.
70+
*
71+
* @param klass The class/module to search.
72+
* @param mid The method ID.
73+
* @return Pointer to the hint, or NULL if none registered.
74+
* The returned pointer is valid until the hint is removed.
75+
*/
76+
const rb_jit_native_call_hint_t *
77+
rb_jit_find_native_call_hint(VALUE klass, ID mid);
78+
79+
RUBY_SYMBOL_EXPORT_END
80+
81+
#endif /* RUBY_JIT_NATIVE_H */

jit.c

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "vm_sync.h"
1717
#include "internal/fixnum.h"
1818
#include "internal/string.h"
19+
#include "ruby/jit_native.h"
1920

2021
enum jit_bindgen_constants {
2122
// Field offsets for the RObject struct
@@ -31,6 +32,9 @@ enum jit_bindgen_constants {
3132
RUBY_OFFSET_EC_INTERRUPT_MASK = offsetof(rb_execution_context_t, interrupt_mask),
3233
RUBY_OFFSET_EC_THREAD_PTR = offsetof(rb_execution_context_t, thread_ptr),
3334
RUBY_OFFSET_EC_RACTOR_ID = offsetof(rb_execution_context_t, ractor_id),
35+
36+
// Field offset for RTypedData.data (used for FFI::Pointer address extraction)
37+
RUBY_OFFSET_RTYPEDDATA_DATA = offsetof(struct RTypedData, data),
3438
};
3539

3640
// Manually bound in rust since this is out-of-range of `int`,
@@ -797,3 +801,250 @@ rb_jit_shape_capacity(shape_id_t shape_id)
797801
{
798802
return RSHAPE_CAPACITY(shape_id);
799803
}
804+
805+
// --- Native call hints for JIT optimization of FFI-style calls ---
806+
//
807+
// Two-level st_table: klass (VALUE) -> st_table(mid (ID) -> hint ptr).
808+
// Populated at runtime by C extensions via rb_jit_hint_native_call().
809+
// Queried by the JIT compiler via rb_jit_find_native_call_hint().
810+
811+
static st_table *jit_native_hints = NULL;
812+
813+
void
814+
rb_jit_hint_native_call(VALUE klass, ID mid, void *func_ptr,
815+
int argc, const int *arg_types, int ret_type)
816+
{
817+
if (argc < 0 || argc > RB_JIT_NATIVE_MAX_ARGS) {
818+
rb_raise(rb_eArgError,
819+
"rb_jit_hint_native_call: argc %d out of range (0..%d)",
820+
argc, RB_JIT_NATIVE_MAX_ARGS);
821+
}
822+
823+
if (!jit_native_hints) {
824+
jit_native_hints = st_init_numtable();
825+
}
826+
827+
// Find or create the inner table for this class
828+
st_data_t inner_val;
829+
st_table *inner;
830+
if (st_lookup(jit_native_hints, (st_data_t)klass, &inner_val)) {
831+
inner = (st_table *)inner_val;
832+
}
833+
else {
834+
inner = st_init_numtable();
835+
st_insert(jit_native_hints, (st_data_t)klass, (st_data_t)inner);
836+
}
837+
838+
rb_jit_native_call_hint_t *hint = xcalloc(1, sizeof(rb_jit_native_call_hint_t));
839+
hint->func_ptr = func_ptr;
840+
hint->argc = argc;
841+
hint->ret_type = ret_type;
842+
for (int i = 0; i < argc; i++) {
843+
hint->arg_types[i] = arg_types[i];
844+
}
845+
846+
// Replace any existing hint for the same method
847+
st_data_t old_val;
848+
if (st_lookup(inner, (st_data_t)mid, &old_val)) {
849+
xfree((void *)old_val);
850+
}
851+
st_insert(inner, (st_data_t)mid, (st_data_t)hint);
852+
}
853+
854+
const rb_jit_native_call_hint_t *
855+
rb_jit_find_native_call_hint(VALUE klass, ID mid)
856+
{
857+
if (!jit_native_hints) return NULL;
858+
859+
st_data_t inner_val;
860+
if (!st_lookup(jit_native_hints, (st_data_t)klass, &inner_val)) return NULL;
861+
862+
st_table *inner = (st_table *)inner_val;
863+
st_data_t hint_val;
864+
if (!st_lookup(inner, (st_data_t)mid, &hint_val)) return NULL;
865+
866+
return (const rb_jit_native_call_hint_t *)hint_val;
867+
}
868+
869+
// --- Typed trampolines for native calls involving floating-point ---
870+
//
871+
// The JIT backend passes all arguments in integer registers, but C functions
872+
// with float/double parameters expect them in FP registers. These trampolines
873+
// bridge the gap: they accept Ruby VALUEs in integer registers, convert to
874+
// native types, call the target function, and box the result back to Ruby.
875+
//
876+
// Each trampoline is named: rb_jit_trampoline_<arg_types>_<ret_type>
877+
// where d = double, f = float, i = int, l = long, v = void.
878+
879+
// double f(double)
880+
VALUE
881+
rb_jit_trampoline_d_d(void *fptr, VALUE a0)
882+
{
883+
double arg0 = rb_float_value(a0);
884+
double result = ((double (*)(double))fptr)(arg0);
885+
return rb_float_new(result);
886+
}
887+
888+
// double f(double, double)
889+
VALUE
890+
rb_jit_trampoline_dd_d(void *fptr, VALUE a0, VALUE a1)
891+
{
892+
double arg0 = rb_float_value(a0);
893+
double arg1 = rb_float_value(a1);
894+
double result = ((double (*)(double, double))fptr)(arg0, arg1);
895+
return rb_float_new(result);
896+
}
897+
898+
// double f(int)
899+
VALUE
900+
rb_jit_trampoline_i_d(void *fptr, VALUE a0)
901+
{
902+
int arg0 = FIX2INT(a0);
903+
double result = ((double (*)(int))fptr)(arg0);
904+
return rb_float_new(result);
905+
}
906+
907+
// double f(int, double)
908+
VALUE
909+
rb_jit_trampoline_id_d(void *fptr, VALUE a0, VALUE a1)
910+
{
911+
int arg0 = FIX2INT(a0);
912+
double arg1 = rb_float_value(a1);
913+
double result = ((double (*)(int, double))fptr)(arg0, arg1);
914+
return rb_float_new(result);
915+
}
916+
917+
// double f(double, int)
918+
VALUE
919+
rb_jit_trampoline_di_d(void *fptr, VALUE a0, VALUE a1)
920+
{
921+
double arg0 = rb_float_value(a0);
922+
int arg1 = FIX2INT(a1);
923+
double result = ((double (*)(double, int))fptr)(arg0, arg1);
924+
return rb_float_new(result);
925+
}
926+
927+
// void f(double)
928+
VALUE
929+
rb_jit_trampoline_d_v(void *fptr, VALUE a0)
930+
{
931+
double arg0 = rb_float_value(a0);
932+
((void (*)(double))fptr)(arg0);
933+
return Qnil;
934+
}
935+
936+
// double f(void)
937+
VALUE
938+
rb_jit_trampoline_v_d(void *fptr)
939+
{
940+
double result = ((double (*)(void))fptr)();
941+
return rb_float_new(result);
942+
}
943+
944+
// --- Generic trampoline for signatures with pointer/string/int args ---
945+
//
946+
// Handles all args that fit in integer registers (no FP). Extracts native
947+
// values from Ruby objects: Fixnum → int/long, String → RSTRING_PTR,
948+
// FFI::Pointer → DATA_PTR address, bool → 0/1.
949+
//
950+
// The hint struct is passed as the first arg so the trampoline knows the
951+
// types. Remaining args are Ruby VALUEs passed through from JIT code.
952+
953+
static uintptr_t
954+
jit_convert_arg(int native_type, VALUE val)
955+
{
956+
switch (native_type) {
957+
case RB_JIT_NATIVE_INT:
958+
case RB_JIT_NATIVE_UINT:
959+
return (uintptr_t)(unsigned int)FIX2INT(val);
960+
case RB_JIT_NATIVE_LONG:
961+
case RB_JIT_NATIVE_ULONG:
962+
case RB_JIT_NATIVE_INT64:
963+
case RB_JIT_NATIVE_UINT64:
964+
return (uintptr_t)FIX2LONG(val);
965+
case RB_JIT_NATIVE_POINTER:
966+
if (FIXNUM_P(val))
967+
return (uintptr_t)FIX2LONG(val);
968+
if (NIL_P(val))
969+
return (uintptr_t)0;
970+
// T_DATA with address at offset 0 (FFI::Pointer, Fiddle::Pointer)
971+
if (RB_TYPE_P(val, T_DATA))
972+
return *(uintptr_t *)RTYPEDDATA_DATA(val);
973+
return (uintptr_t)NUM2LONG(rb_funcall(val, rb_intern("to_i"), 0));
974+
case RB_JIT_NATIVE_STRING:
975+
return (uintptr_t)RSTRING_PTR(val);
976+
case RB_JIT_NATIVE_BOOL:
977+
return RTEST(val) ? 1 : 0;
978+
default:
979+
return 0;
980+
}
981+
}
982+
983+
typedef uintptr_t (*generic_fn0_t)(void);
984+
typedef uintptr_t (*generic_fn1_t)(uintptr_t);
985+
typedef uintptr_t (*generic_fn2_t)(uintptr_t, uintptr_t);
986+
typedef uintptr_t (*generic_fn3_t)(uintptr_t, uintptr_t, uintptr_t);
987+
typedef uintptr_t (*generic_fn4_t)(uintptr_t, uintptr_t, uintptr_t, uintptr_t);
988+
typedef uintptr_t (*generic_fn5_t)(uintptr_t, uintptr_t, uintptr_t, uintptr_t, uintptr_t);
989+
typedef uintptr_t (*generic_fn6_t)(uintptr_t, uintptr_t, uintptr_t, uintptr_t, uintptr_t, uintptr_t);
990+
991+
static VALUE
992+
jit_box_result(int ret_type, uintptr_t result)
993+
{
994+
switch (ret_type) {
995+
case RB_JIT_NATIVE_INT:
996+
return INT2FIX((int)(intptr_t)result);
997+
case RB_JIT_NATIVE_UINT:
998+
return UINT2NUM((unsigned int)result);
999+
case RB_JIT_NATIVE_LONG:
1000+
case RB_JIT_NATIVE_INT64:
1001+
return LONG2FIX((long)result);
1002+
case RB_JIT_NATIVE_ULONG:
1003+
case RB_JIT_NATIVE_UINT64:
1004+
return ULONG2NUM((unsigned long)result);
1005+
case RB_JIT_NATIVE_BOOL:
1006+
return result ? Qtrue : Qfalse;
1007+
case RB_JIT_NATIVE_VOID:
1008+
default:
1009+
return Qnil;
1010+
}
1011+
}
1012+
1013+
// Variadic-cfunc-compatible trampoline.
1014+
// Signature matches CCallVariadic convention: func(int argc, VALUE *argv, VALUE recv).
1015+
// Looks up the hint from the current method's CME using the cfp.
1016+
VALUE
1017+
rb_jit_trampoline_generic(int argc, const VALUE *argv, VALUE recv)
1018+
{
1019+
rb_execution_context_t *ec = GET_EC();
1020+
const rb_callable_method_entry_t *cme = rb_vm_frame_method_entry(ec->cfp);
1021+
VALUE klass = cme->defined_class;
1022+
ID mid = cme->called_id;
1023+
1024+
const rb_jit_native_call_hint_t *hint =
1025+
rb_jit_find_native_call_hint(klass, mid);
1026+
if (!hint) {
1027+
rb_raise(rb_eRuntimeError, "rb_jit_trampoline_generic: no hint found");
1028+
}
1029+
1030+
uintptr_t a[RB_JIT_NATIVE_MAX_ARGS];
1031+
void *fp = hint->func_ptr;
1032+
1033+
for (int i = 0; i < hint->argc && i < argc; i++) {
1034+
a[i] = jit_convert_arg(hint->arg_types[i], argv[i]);
1035+
}
1036+
1037+
uintptr_t result;
1038+
switch (hint->argc) {
1039+
case 0: result = ((generic_fn0_t)fp)(); break;
1040+
case 1: result = ((generic_fn1_t)fp)(a[0]); break;
1041+
case 2: result = ((generic_fn2_t)fp)(a[0], a[1]); break;
1042+
case 3: result = ((generic_fn3_t)fp)(a[0], a[1], a[2]); break;
1043+
case 4: result = ((generic_fn4_t)fp)(a[0], a[1], a[2], a[3]); break;
1044+
case 5: result = ((generic_fn5_t)fp)(a[0], a[1], a[2], a[3], a[4]); break;
1045+
case 6: result = ((generic_fn6_t)fp)(a[0], a[1], a[2], a[3], a[4], a[5]); break;
1046+
default: result = 0; break;
1047+
}
1048+
1049+
return jit_box_result(hint->ret_type, result);
1050+
}

0 commit comments

Comments
 (0)