Skip to content

Commit e51f9e9

Browse files
tagomorisnobu
authored andcommitted
rb_ext_resolve_symbol: C API to resolve and return externed symbols [Feature #20005]
This is a C API for extensions to resolve and get function symbols of other extensions. Extensions can check the expected symbol is correctly loaded and accessible, and use it if it is available. Otherwise, extensions can raise their own error to guide users to setup their environments correctly and what's missing.
1 parent 8a37df8 commit e51f9e9

File tree

18 files changed

+277
-9
lines changed

18 files changed

+277
-9
lines changed

common.mk

+1
Original file line numberDiff line numberDiff line change
@@ -8542,6 +8542,7 @@ load.$(OBJEXT): $(top_srcdir)/internal/dir.h
85428542
load.$(OBJEXT): $(top_srcdir)/internal/error.h
85438543
load.$(OBJEXT): $(top_srcdir)/internal/file.h
85448544
load.$(OBJEXT): $(top_srcdir)/internal/gc.h
8545+
load.$(OBJEXT): $(top_srcdir)/internal/hash.h
85458546
load.$(OBJEXT): $(top_srcdir)/internal/imemo.h
85468547
load.$(OBJEXT): $(top_srcdir)/internal/load.h
85478548
load.$(OBJEXT): $(top_srcdir)/internal/parse.h

dln.c

+2-2
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,8 @@ dln_symbol(void *handle, const char *symbol)
463463
}
464464
if (handle == NULL) {
465465
# if defined(USE_DLN_DLOPEN)
466-
handle = dlopen(NULL, 0);
467-
# elif defined(_WIN32) && defined(RUBY_EXPORT)
466+
handle = dlopen(NULL, RTLD_LAZY | RTLD_GLOBAL);
467+
# elif defined(_WIN32)
468468
handle = rb_libruby_handle();
469469
# else
470470
return NULL;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
create_makefile('-test-/load/resolve_symbol_resolver')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#include <ruby.h>
2+
#include "ruby/internal/intern/load.h"
3+
4+
typedef VALUE(*target_func)(VALUE);
5+
6+
static target_func rst_any_method;
7+
8+
VALUE
9+
rsr_any_method(VALUE klass)
10+
{
11+
return rst_any_method((VALUE)NULL);
12+
}
13+
14+
VALUE
15+
rsr_try_resolve_fname(VALUE klass)
16+
{
17+
target_func rst_something_missing =
18+
(target_func) rb_ext_resolve_symbol("-test-/load/resolve_symbol_missing", "rst_any_method");
19+
if (rst_something_missing == NULL) {
20+
// This should be done in Init_*, so the error is LoadError
21+
rb_raise(rb_eLoadError, "symbol not found: missing fname");
22+
}
23+
return Qtrue;
24+
}
25+
26+
VALUE
27+
rsr_try_resolve_sname(VALUE klass)
28+
{
29+
target_func rst_something_missing =
30+
(target_func)rb_ext_resolve_symbol("-test-/load/resolve_symbol_target", "rst_something_missing");
31+
if (rst_something_missing == NULL) {
32+
// This should be done in Init_*, so the error is LoadError
33+
rb_raise(rb_eLoadError, "symbol not found: missing sname");
34+
}
35+
return Qtrue;
36+
}
37+
38+
void
39+
Init_resolve_symbol_resolver(void)
40+
{
41+
VALUE mod = rb_define_module("ResolveSymbolResolver");
42+
rb_define_singleton_method(mod, "any_method", rsr_any_method, 0);
43+
rb_define_singleton_method(mod, "try_resolve_fname", rsr_try_resolve_fname, 0);
44+
rb_define_singleton_method(mod, "try_resolve_sname", rsr_try_resolve_sname, 0);
45+
46+
rst_any_method = (target_func)rb_ext_resolve_symbol("-test-/load/resolve_symbol_target", "rst_any_method");
47+
if (rst_any_method == NULL) {
48+
rb_raise(rb_eLoadError, "resolve_symbol_target is not loaded");
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
create_makefile('-test-/load/resolve_symbol_target')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <ruby.h>
2+
#include "resolve_symbol_target.h"
3+
4+
VALUE
5+
rst_any_method(VALUE klass)
6+
{
7+
return rb_str_new_cstr("from target");
8+
}
9+
10+
void
11+
Init_resolve_symbol_target(void)
12+
{
13+
VALUE mod = rb_define_module("ResolveSymbolTarget");
14+
rb_define_singleton_method(mod, "any_method", rst_any_method, 0);
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
LIBRARY resolve_symbol_target
2+
EXPORTS
3+
Init_resolve_symbol_target
4+
rst_any_method
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#include <ruby.h>
2+
#include "ruby/internal/dllexport.h"
3+
4+
RUBY_EXTERN VALUE rst_any_method(VALUE);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
create_makefile('-test-/load/stringify_symbols')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#include <ruby.h>
2+
#include "ruby/internal/intern/load.h"
3+
#include "ruby/util.h"
4+
5+
#if SIZEOF_INTPTR_T == SIZEOF_LONG_LONG
6+
# define UINTPTR2NUM ULL2NUM
7+
#elif SIZEOF_INTPTR_T == SIZEOF_LONG
8+
# define UINTPTR2NUM ULONG2NUM
9+
#else
10+
# define UINTPTR2NUM UINT2NUM
11+
#endif
12+
13+
static VALUE
14+
stringify_symbol(VALUE klass, VALUE fname, VALUE sname)
15+
{
16+
void *ptr = rb_ext_resolve_symbol(StringValueCStr(fname), StringValueCStr(sname));
17+
if (ptr == NULL) {
18+
return Qnil;
19+
}
20+
uintptr_t uintptr = (uintptr_t)ptr;
21+
return UINTPTR2NUM(uintptr);
22+
}
23+
24+
void
25+
Init_stringify_symbols(void)
26+
{
27+
VALUE mod = rb_define_module("StringifySymbols");
28+
rb_define_singleton_method(mod, "stringify_symbol", stringify_symbol, 2);
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
create_makefile('-test-/load/stringify_target')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <ruby.h>
2+
#include "stringify_target.h"
3+
4+
VALUE
5+
stt_any_method(VALUE klass)
6+
{
7+
return rb_str_new_cstr("from target");
8+
}
9+
10+
void
11+
Init_stringify_target(void)
12+
{
13+
VALUE mod = rb_define_module("StringifyTarget");
14+
rb_define_singleton_method(mod, "any_method", stt_any_method, 0);
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
LIBRARY stringify_target
2+
EXPORTS
3+
Init_stringify_target
4+
stt_any_method
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#include <ruby.h>
2+
#include "ruby/internal/dllexport.h"
3+
4+
RUBY_EXTERN VALUE stt_any_method(VALUE);

include/ruby/internal/intern/load.h

+37
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,43 @@ VALUE rb_f_require(VALUE self, VALUE feature);
176176
*/
177177
VALUE rb_require_string(VALUE feature);
178178

179+
/**
180+
* Resolves and returns a symbol of a function in the native extension
181+
* specified by the feature and symbol names. Extensions will use this function
182+
* to access the symbols provided by other native extensions.
183+
*
184+
* @param[in] feature Name of a feature, e.g. `"json"`.
185+
* @param[in] symbol Name of a symbol defined by the feature.
186+
* @return The resolved symbol of a function, defined and externed by the
187+
* specified feature. It may be NULL if the feature is not loaded,
188+
* the feature is not extension, or the symbol is not found.
189+
*/
190+
void *rb_ext_resolve_symbol(const char *feature, const char *symbol);
191+
192+
/**
193+
* This macro is to provide backwards compatibility. It provides a way to
194+
* define function prototypes and resolving function symbols in a safe way.
195+
*
196+
* ```CXX
197+
* // prototypes
198+
* #ifdef HAVE_RB_EXT_RESOLVE_SYMBOL
199+
* VALUE *(*other_extension_func)(VALUE,VALUE);
200+
* #else
201+
* VALUE other_extension_func(VALUE);
202+
* #endif
203+
*
204+
* // in Init_xxx()
205+
* #ifdef HAVE_RB_EXT_RESOLVE_SYMBOL
206+
* other_extension_func = \
207+
* (VALUE(*)(VALUE,VALUE))rb_ext_resolve_symbol(fname, sym_name);
208+
* if (other_extension_func == NULL) {
209+
* // raise your own error
210+
* }
211+
* #endif
212+
* ```
213+
*/
214+
#define HAVE_RB_EXT_RESOLVE_SYMBOL 1
215+
179216
/**
180217
* @name extension configuration
181218
* @{

load.c

+49-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "internal/dir.h"
99
#include "internal/error.h"
1010
#include "internal/file.h"
11+
#include "internal/hash.h"
1112
#include "internal/load.h"
1213
#include "internal/ruby_parser.h"
1314
#include "internal/thread.h"
@@ -18,12 +19,22 @@
1819
#include "ruby/encoding.h"
1920
#include "ruby/util.h"
2021

21-
static VALUE ruby_dln_librefs;
22+
static VALUE ruby_dln_libmap;
2223

2324
#define IS_RBEXT(e) (strcmp((e), ".rb") == 0)
2425
#define IS_SOEXT(e) (strcmp((e), ".so") == 0 || strcmp((e), ".o") == 0)
2526
#define IS_DLEXT(e) (strcmp((e), DLEXT) == 0)
2627

28+
#if SIZEOF_VALUE <= SIZEOF_LONG
29+
# define SVALUE2NUM(x) LONG2NUM((long)(x))
30+
# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LONG(x)
31+
#elif SIZEOF_VALUE <= SIZEOF_LONG_LONG
32+
# define SVALUE2NUM(x) LL2NUM((LONG_LONG)(x))
33+
# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LL(x)
34+
#else
35+
# error Need integer for VALUE
36+
#endif
37+
2738
enum {
2839
loadable_ext_rb = (0+ /* .rb extension is the first in both tables */
2940
1) /* offset by rb_find_file_ext() */
@@ -1225,7 +1236,7 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa
12251236
ec->errinfo = Qnil; /* ensure */
12261237
th->top_wrapper = 0;
12271238
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
1228-
long handle;
1239+
VALUE handle;
12291240
int found;
12301241

12311242
RUBY_DTRACE_HOOK(FIND_REQUIRE_ENTRY, RSTRING_PTR(fname));
@@ -1256,9 +1267,9 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa
12561267
case 's':
12571268
reset_ext_config = true;
12581269
ext_config_push(th, &prev_ext_config);
1259-
handle = (long)rb_vm_call_cfunc(rb_vm_top_self(), load_ext,
1260-
path, VM_BLOCK_HANDLER_NONE, path);
1261-
rb_ary_push(ruby_dln_librefs, LONG2NUM(handle));
1270+
handle = rb_vm_call_cfunc(rb_vm_top_self(), load_ext,
1271+
path, VM_BLOCK_HANDLER_NONE, path);
1272+
rb_hash_aset(ruby_dln_libmap, path, SVALUE2NUM((SIGNED_VALUE)handle));
12621273
break;
12631274
}
12641275
result = TAG_RETURN;
@@ -1518,6 +1529,37 @@ rb_f_autoload_p(int argc, VALUE *argv, VALUE obj)
15181529
return rb_mod_autoload_p(argc, argv, klass);
15191530
}
15201531

1532+
void *
1533+
rb_ext_resolve_symbol(const char* fname, const char* symbol)
1534+
{
1535+
VALUE handle;
1536+
VALUE resolved;
1537+
VALUE path;
1538+
char *ext;
1539+
VALUE fname_str = rb_str_new_cstr(fname);
1540+
1541+
resolved = rb_resolve_feature_path((VALUE)NULL, fname_str);
1542+
if (NIL_P(resolved)) {
1543+
ext = strrchr(fname, '.');
1544+
if (!ext || !IS_SOEXT(ext)) {
1545+
rb_str_cat_cstr(fname_str, ".so");
1546+
}
1547+
if (rb_feature_p(GET_VM(), fname, 0, FALSE, FALSE, 0)) {
1548+
return dln_symbol(NULL, symbol);
1549+
}
1550+
return NULL;
1551+
}
1552+
if (RARRAY_LEN(resolved) != 2 || rb_ary_entry(resolved, 0) != ID2SYM(rb_intern("so"))) {
1553+
return NULL;
1554+
}
1555+
path = rb_ary_entry(resolved, 1);
1556+
handle = rb_hash_lookup(ruby_dln_libmap, path);
1557+
if (NIL_P(handle)) {
1558+
return NULL;
1559+
}
1560+
return dln_symbol((void *)NUM2SVALUE(handle), symbol);
1561+
}
1562+
15211563
void
15221564
Init_load(void)
15231565
{
@@ -1552,6 +1594,6 @@ Init_load(void)
15521594
rb_define_global_function("autoload", rb_f_autoload, 2);
15531595
rb_define_global_function("autoload?", rb_f_autoload_p, -1);
15541596

1555-
ruby_dln_librefs = rb_ary_hidden_new(0);
1556-
rb_gc_register_mark_object(ruby_dln_librefs);
1597+
ruby_dln_libmap = rb_hash_new_with_size(0);
1598+
rb_gc_register_mark_object(ruby_dln_libmap);
15571599
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
require 'test/unit'
3+
4+
class Test_Load_ResolveSymbol < Test::Unit::TestCase
5+
def test_load_resolve_symbol_resolver
6+
feature = "Feature #20005"
7+
assert_raise(LoadError, "resolve_symbol_target is not loaded") {
8+
require '-test-/load/resolve_symbol_resolver'
9+
}
10+
require '-test-/load/resolve_symbol_target'
11+
assert_nothing_raised(LoadError, "#{feature} resolver can be loaded") {
12+
require '-test-/load/resolve_symbol_resolver'
13+
}
14+
assert_not_nil ResolveSymbolResolver
15+
assert_equal "from target", ResolveSymbolResolver.any_method
16+
17+
assert_raise(LoadError, "tries to resolve missing feature name, and it should raise LoadError") {
18+
ResolveSymbolResolver.try_resolve_fname
19+
}
20+
assert_raise(LoadError, "tries to resolve missing symbol name, and it should raise LoadError") {
21+
ResolveSymbolResolver.try_resolve_sname
22+
}
23+
end
24+
end
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
require 'test/unit'
3+
4+
class Test_Load_stringify_symbols < Test::Unit::TestCase
5+
def test_load_stringify_symbol_required_extensions
6+
require '-test-/load/stringify_symbols'
7+
require '-test-/load/stringify_target'
8+
r1 = StringifySymbols.stringify_symbol("-test-/load/stringify_target", "stt_any_method")
9+
assert_not_nil r1
10+
r2 = StringifySymbols.stringify_symbol("-test-/load/stringify_target.so", "stt_any_method")
11+
assert_equal r1, r2, "resolved symbols should be equal even with or without .so suffix"
12+
end
13+
14+
def test_load_stringify_symbol_statically_linked
15+
require '-test-/load/stringify_symbols'
16+
# "complex.so" is actually not a statically linked extension.
17+
# But it is registered in $LOADED_FEATURES, so it can be a target of this test.
18+
r1 = StringifySymbols.stringify_symbol("complex", "rb_complex_minus")
19+
assert_not_nil r1
20+
r2 = StringifySymbols.stringify_symbol("complex.so", "rb_complex_minus")
21+
assert_equal r1, r2
22+
end
23+
24+
def test_load_stringify_symbol_missing_target
25+
require '-test-/load/stringify_symbols'
26+
r1 = assert_nothing_raised {
27+
StringifySymbols.stringify_symbol("something_missing", "unknown_method")
28+
}
29+
assert_nil r1
30+
r2 = assert_nothing_raised {
31+
StringifySymbols.stringify_symbol("complex.so", "unknown_method")
32+
}
33+
assert_nil r2
34+
end
35+
end

0 commit comments

Comments
 (0)