Skip to content

Commit

Permalink
rb_ext_resolve_symbol: C API to resolve and return externed symbols […
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
tagomoris authored and nobu committed Dec 14, 2023
1 parent 8a37df8 commit e51f9e9
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 9 deletions.
1 change: 1 addition & 0 deletions common.mk
Expand Up @@ -8542,6 +8542,7 @@ load.$(OBJEXT): $(top_srcdir)/internal/dir.h
load.$(OBJEXT): $(top_srcdir)/internal/error.h
load.$(OBJEXT): $(top_srcdir)/internal/file.h
load.$(OBJEXT): $(top_srcdir)/internal/gc.h
load.$(OBJEXT): $(top_srcdir)/internal/hash.h
load.$(OBJEXT): $(top_srcdir)/internal/imemo.h
load.$(OBJEXT): $(top_srcdir)/internal/load.h
load.$(OBJEXT): $(top_srcdir)/internal/parse.h
Expand Down
4 changes: 2 additions & 2 deletions dln.c
Expand Up @@ -463,8 +463,8 @@ dln_symbol(void *handle, const char *symbol)
}
if (handle == NULL) {
# if defined(USE_DLN_DLOPEN)
handle = dlopen(NULL, 0);
# elif defined(_WIN32) && defined(RUBY_EXPORT)
handle = dlopen(NULL, RTLD_LAZY | RTLD_GLOBAL);
# elif defined(_WIN32)
handle = rb_libruby_handle();
# else
return NULL;
Expand Down
1 change: 1 addition & 0 deletions ext/-test-/load/resolve_symbol_resolver/extconf.rb
@@ -0,0 +1 @@
create_makefile('-test-/load/resolve_symbol_resolver')
50 changes: 50 additions & 0 deletions ext/-test-/load/resolve_symbol_resolver/resolve_symbol_resolver.c
@@ -0,0 +1,50 @@
#include <ruby.h>
#include "ruby/internal/intern/load.h"

typedef VALUE(*target_func)(VALUE);

static target_func rst_any_method;

VALUE
rsr_any_method(VALUE klass)
{
return rst_any_method((VALUE)NULL);
}

VALUE
rsr_try_resolve_fname(VALUE klass)
{
target_func rst_something_missing =
(target_func) rb_ext_resolve_symbol("-test-/load/resolve_symbol_missing", "rst_any_method");
if (rst_something_missing == NULL) {
// This should be done in Init_*, so the error is LoadError
rb_raise(rb_eLoadError, "symbol not found: missing fname");
}
return Qtrue;
}

VALUE
rsr_try_resolve_sname(VALUE klass)
{
target_func rst_something_missing =
(target_func)rb_ext_resolve_symbol("-test-/load/resolve_symbol_target", "rst_something_missing");
if (rst_something_missing == NULL) {
// This should be done in Init_*, so the error is LoadError
rb_raise(rb_eLoadError, "symbol not found: missing sname");
}
return Qtrue;
}

void
Init_resolve_symbol_resolver(void)
{
VALUE mod = rb_define_module("ResolveSymbolResolver");
rb_define_singleton_method(mod, "any_method", rsr_any_method, 0);
rb_define_singleton_method(mod, "try_resolve_fname", rsr_try_resolve_fname, 0);
rb_define_singleton_method(mod, "try_resolve_sname", rsr_try_resolve_sname, 0);

rst_any_method = (target_func)rb_ext_resolve_symbol("-test-/load/resolve_symbol_target", "rst_any_method");
if (rst_any_method == NULL) {
rb_raise(rb_eLoadError, "resolve_symbol_target is not loaded");
}
}
1 change: 1 addition & 0 deletions ext/-test-/load/resolve_symbol_target/extconf.rb
@@ -0,0 +1 @@
create_makefile('-test-/load/resolve_symbol_target')
15 changes: 15 additions & 0 deletions ext/-test-/load/resolve_symbol_target/resolve_symbol_target.c
@@ -0,0 +1,15 @@
#include <ruby.h>
#include "resolve_symbol_target.h"

VALUE
rst_any_method(VALUE klass)
{
return rb_str_new_cstr("from target");
}

void
Init_resolve_symbol_target(void)
{
VALUE mod = rb_define_module("ResolveSymbolTarget");
rb_define_singleton_method(mod, "any_method", rst_any_method, 0);
}
@@ -0,0 +1,4 @@
LIBRARY resolve_symbol_target
EXPORTS
Init_resolve_symbol_target
rst_any_method
4 changes: 4 additions & 0 deletions ext/-test-/load/resolve_symbol_target/resolve_symbol_target.h
@@ -0,0 +1,4 @@
#include <ruby.h>
#include "ruby/internal/dllexport.h"

RUBY_EXTERN VALUE rst_any_method(VALUE);
1 change: 1 addition & 0 deletions ext/-test-/load/stringify_symbols/extconf.rb
@@ -0,0 +1 @@
create_makefile('-test-/load/stringify_symbols')
29 changes: 29 additions & 0 deletions ext/-test-/load/stringify_symbols/stringify_symbols.c
@@ -0,0 +1,29 @@
#include <ruby.h>
#include "ruby/internal/intern/load.h"
#include "ruby/util.h"

#if SIZEOF_INTPTR_T == SIZEOF_LONG_LONG
# define UINTPTR2NUM ULL2NUM
#elif SIZEOF_INTPTR_T == SIZEOF_LONG
# define UINTPTR2NUM ULONG2NUM
#else
# define UINTPTR2NUM UINT2NUM
#endif

static VALUE
stringify_symbol(VALUE klass, VALUE fname, VALUE sname)
{
void *ptr = rb_ext_resolve_symbol(StringValueCStr(fname), StringValueCStr(sname));
if (ptr == NULL) {
return Qnil;
}
uintptr_t uintptr = (uintptr_t)ptr;
return UINTPTR2NUM(uintptr);
}

void
Init_stringify_symbols(void)
{
VALUE mod = rb_define_module("StringifySymbols");
rb_define_singleton_method(mod, "stringify_symbol", stringify_symbol, 2);
}
1 change: 1 addition & 0 deletions ext/-test-/load/stringify_target/extconf.rb
@@ -0,0 +1 @@
create_makefile('-test-/load/stringify_target')
15 changes: 15 additions & 0 deletions ext/-test-/load/stringify_target/stringify_target.c
@@ -0,0 +1,15 @@
#include <ruby.h>
#include "stringify_target.h"

VALUE
stt_any_method(VALUE klass)
{
return rb_str_new_cstr("from target");
}

void
Init_stringify_target(void)
{
VALUE mod = rb_define_module("StringifyTarget");
rb_define_singleton_method(mod, "any_method", stt_any_method, 0);
}
4 changes: 4 additions & 0 deletions ext/-test-/load/stringify_target/stringify_target.def
@@ -0,0 +1,4 @@
LIBRARY stringify_target
EXPORTS
Init_stringify_target
stt_any_method
4 changes: 4 additions & 0 deletions ext/-test-/load/stringify_target/stringify_target.h
@@ -0,0 +1,4 @@
#include <ruby.h>
#include "ruby/internal/dllexport.h"

RUBY_EXTERN VALUE stt_any_method(VALUE);
37 changes: 37 additions & 0 deletions include/ruby/internal/intern/load.h
Expand Up @@ -176,6 +176,43 @@ VALUE rb_f_require(VALUE self, VALUE feature);
*/
VALUE rb_require_string(VALUE feature);

/**
* Resolves and returns a symbol of a function in the native extension
* specified by the feature and symbol names. Extensions will use this function
* to access the symbols provided by other native extensions.
*
* @param[in] feature Name of a feature, e.g. `"json"`.
* @param[in] symbol Name of a symbol defined by the feature.
* @return The resolved symbol of a function, defined and externed by the
* specified feature. It may be NULL if the feature is not loaded,
* the feature is not extension, or the symbol is not found.
*/
void *rb_ext_resolve_symbol(const char *feature, const char *symbol);

/**
* This macro is to provide backwards compatibility. It provides a way to
* define function prototypes and resolving function symbols in a safe way.
*
* ```CXX
* // prototypes
* #ifdef HAVE_RB_EXT_RESOLVE_SYMBOL
* VALUE *(*other_extension_func)(VALUE,VALUE);
* #else
* VALUE other_extension_func(VALUE);
* #endif
*
* // in Init_xxx()
* #ifdef HAVE_RB_EXT_RESOLVE_SYMBOL
* other_extension_func = \
* (VALUE(*)(VALUE,VALUE))rb_ext_resolve_symbol(fname, sym_name);
* if (other_extension_func == NULL) {
* // raise your own error
* }
* #endif
* ```
*/
#define HAVE_RB_EXT_RESOLVE_SYMBOL 1

/**
* @name extension configuration
* @{
Expand Down
56 changes: 49 additions & 7 deletions load.c
Expand Up @@ -8,6 +8,7 @@
#include "internal/dir.h"
#include "internal/error.h"
#include "internal/file.h"
#include "internal/hash.h"
#include "internal/load.h"
#include "internal/ruby_parser.h"
#include "internal/thread.h"
Expand All @@ -18,12 +19,22 @@
#include "ruby/encoding.h"
#include "ruby/util.h"

static VALUE ruby_dln_librefs;
static VALUE ruby_dln_libmap;

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

#if SIZEOF_VALUE <= SIZEOF_LONG
# define SVALUE2NUM(x) LONG2NUM((long)(x))
# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LONG(x)
#elif SIZEOF_VALUE <= SIZEOF_LONG_LONG
# define SVALUE2NUM(x) LL2NUM((LONG_LONG)(x))
# define NUM2SVALUE(x) (SIGNED_VALUE)NUM2LL(x)
#else
# error Need integer for VALUE
#endif

enum {
loadable_ext_rb = (0+ /* .rb extension is the first in both tables */
1) /* offset by rb_find_file_ext() */
Expand Down Expand Up @@ -1225,7 +1236,7 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa
ec->errinfo = Qnil; /* ensure */
th->top_wrapper = 0;
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
long handle;
VALUE handle;
int found;

RUBY_DTRACE_HOOK(FIND_REQUIRE_ENTRY, RSTRING_PTR(fname));
Expand Down Expand Up @@ -1256,9 +1267,9 @@ require_internal(rb_execution_context_t *ec, VALUE fname, int exception, bool wa
case 's':
reset_ext_config = true;
ext_config_push(th, &prev_ext_config);
handle = (long)rb_vm_call_cfunc(rb_vm_top_self(), load_ext,
path, VM_BLOCK_HANDLER_NONE, path);
rb_ary_push(ruby_dln_librefs, LONG2NUM(handle));
handle = rb_vm_call_cfunc(rb_vm_top_self(), load_ext,
path, VM_BLOCK_HANDLER_NONE, path);
rb_hash_aset(ruby_dln_libmap, path, SVALUE2NUM((SIGNED_VALUE)handle));
break;
}
result = TAG_RETURN;
Expand Down Expand Up @@ -1518,6 +1529,37 @@ rb_f_autoload_p(int argc, VALUE *argv, VALUE obj)
return rb_mod_autoload_p(argc, argv, klass);
}

void *
rb_ext_resolve_symbol(const char* fname, const char* symbol)
{
VALUE handle;
VALUE resolved;
VALUE path;
char *ext;
VALUE fname_str = rb_str_new_cstr(fname);

resolved = rb_resolve_feature_path((VALUE)NULL, fname_str);
if (NIL_P(resolved)) {
ext = strrchr(fname, '.');
if (!ext || !IS_SOEXT(ext)) {
rb_str_cat_cstr(fname_str, ".so");
}
if (rb_feature_p(GET_VM(), fname, 0, FALSE, FALSE, 0)) {
return dln_symbol(NULL, symbol);
}
return NULL;
}
if (RARRAY_LEN(resolved) != 2 || rb_ary_entry(resolved, 0) != ID2SYM(rb_intern("so"))) {
return NULL;
}
path = rb_ary_entry(resolved, 1);
handle = rb_hash_lookup(ruby_dln_libmap, path);
if (NIL_P(handle)) {
return NULL;
}
return dln_symbol((void *)NUM2SVALUE(handle), symbol);
}

void
Init_load(void)
{
Expand Down Expand Up @@ -1552,6 +1594,6 @@ Init_load(void)
rb_define_global_function("autoload", rb_f_autoload, 2);
rb_define_global_function("autoload?", rb_f_autoload_p, -1);

ruby_dln_librefs = rb_ary_hidden_new(0);
rb_gc_register_mark_object(ruby_dln_librefs);
ruby_dln_libmap = rb_hash_new_with_size(0);
rb_gc_register_mark_object(ruby_dln_libmap);
}
24 changes: 24 additions & 0 deletions test/-ext-/load/test_resolve_symbol.rb
@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'test/unit'

class Test_Load_ResolveSymbol < Test::Unit::TestCase
def test_load_resolve_symbol_resolver
feature = "Feature #20005"
assert_raise(LoadError, "resolve_symbol_target is not loaded") {
require '-test-/load/resolve_symbol_resolver'
}
require '-test-/load/resolve_symbol_target'
assert_nothing_raised(LoadError, "#{feature} resolver can be loaded") {
require '-test-/load/resolve_symbol_resolver'
}
assert_not_nil ResolveSymbolResolver
assert_equal "from target", ResolveSymbolResolver.any_method

assert_raise(LoadError, "tries to resolve missing feature name, and it should raise LoadError") {
ResolveSymbolResolver.try_resolve_fname
}
assert_raise(LoadError, "tries to resolve missing symbol name, and it should raise LoadError") {
ResolveSymbolResolver.try_resolve_sname
}
end
end
35 changes: 35 additions & 0 deletions test/-ext-/load/test_stringify_symbols.rb
@@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'test/unit'

class Test_Load_stringify_symbols < Test::Unit::TestCase
def test_load_stringify_symbol_required_extensions
require '-test-/load/stringify_symbols'
require '-test-/load/stringify_target'
r1 = StringifySymbols.stringify_symbol("-test-/load/stringify_target", "stt_any_method")
assert_not_nil r1
r2 = StringifySymbols.stringify_symbol("-test-/load/stringify_target.so", "stt_any_method")
assert_equal r1, r2, "resolved symbols should be equal even with or without .so suffix"
end

def test_load_stringify_symbol_statically_linked
require '-test-/load/stringify_symbols'
# "complex.so" is actually not a statically linked extension.
# But it is registered in $LOADED_FEATURES, so it can be a target of this test.
r1 = StringifySymbols.stringify_symbol("complex", "rb_complex_minus")
assert_not_nil r1
r2 = StringifySymbols.stringify_symbol("complex.so", "rb_complex_minus")
assert_equal r1, r2
end

def test_load_stringify_symbol_missing_target
require '-test-/load/stringify_symbols'
r1 = assert_nothing_raised {
StringifySymbols.stringify_symbol("something_missing", "unknown_method")
}
assert_nil r1
r2 = assert_nothing_raised {
StringifySymbols.stringify_symbol("complex.so", "unknown_method")
}
assert_nil r2
end
end

0 comments on commit e51f9e9

Please sign in to comment.