Skip to content

Commit

Permalink
Basic support for loading modules (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeroen authored Oct 5, 2024
1 parent e3bfdc3 commit 32be402
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 8 deletions.
14 changes: 6 additions & 8 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ jobs:
- {os: macOS-13, r: 'release'}
- {os: macOS-14, r: 'release'}
- {os: macOS-latest, r: 'release', disable-static: 'disable-static'}
- {os: windows-latest, r: '4.0'}
- {os: windows-latest, r: '4.1'}
- {os: windows-latest, r: '4.2'}
- {os: windows-latest, r: '4.3'}
- {os: windows-latest, r: 'release'}
- {os: windows-latest, r: '4.4'}
- {os: windows-2022, r: 'devel'}
- {os: ubuntu-24.04, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-24.04, r: 'release', disable-static: 'disable-static'}
- {os: ubuntu-22.04, r: 'release', disable-static: 'disable-static'}
- {os: ubuntu-20.04, r: 'release'}
- {os: ubuntu-20.04, r: 'release', disable-static: 'disable-static'}

env:
Expand All @@ -35,7 +37,7 @@ jobs:
DISABLE_STATIC_LIBV8: ${{ matrix.config.disable-static }}

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- if: runner.os == 'macos' && matrix.config.disable-static
run: brew install v8 || true
Expand All @@ -54,7 +56,3 @@ jobs:
needs: check

- uses: r-lib/actions/check-r-package@v2
env:
_R_CALLS_INVALID_NUMERIC_VERSION_: true
with:
upload-snapshots: true
86 changes: 86 additions & 0 deletions src/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
#endif
#endif

#if !defined(ISNODEJS) || NODEJS_LTS_API > 16
#define FixedArrayParam ,v8::Local<v8::FixedArray> import_arributes
#else
#define FixedArrayParam
#endif

#if V8_VERSION_TOTAL < 803
#define PerformMicrotaskCheckpoint RunMicrotasks
#endif
Expand Down Expand Up @@ -47,6 +53,15 @@ void ctx_finalizer(ctx_type* context ){
static v8::Isolate* isolate = NULL;
static v8::Platform* platformptr = NULL;

static std::string read_text(std::string filename) {
std::ifstream t(filename);
if(t.fail())
throw std::runtime_error("Failed to open file: " + filename);
std::stringstream buffer;
buffer << t.rdbuf();
return buffer.str();
}

// Extracts a C string from a V8 Utf8Value.
static const char* ToCString(const v8::String::Utf8Value& value) {
return *value ? *value : "<string conversion failed>";
Expand All @@ -66,6 +81,76 @@ static void fatal_cb(const char* location, const char* message){
REprintf("V8 FATAL ERROR in %s: %s", location, message);
}

static v8::Local<v8::Module> read_module(std::string filename, v8::Local<v8::Context> context);

static v8::MaybeLocal<v8::Module> ResolveModuleCallback(v8::Local<v8::Context> context, v8::Local<v8::String> specifier
FixedArrayParam, v8::Local<v8::Module> referrer) {
v8::String::Utf8Value name(context->GetIsolate(), specifier);
return read_module(*name, context);
}

static v8::MaybeLocal<v8::Promise> dynamic_module_loader(v8::Local<v8::Context> context, v8::Local<v8::String> specifier) {
v8::Local<v8::Promise::Resolver> resolver = v8::Promise::Resolver::New(context).ToLocalChecked();
v8::MaybeLocal<v8::Promise> promise(resolver->GetPromise());
v8::String::Utf8Value name(context->GetIsolate(), specifier);
try {
v8::Local<v8::Module> module = read_module(*name, context);
v8::Local<v8::Value> retValue;
if (!module->Evaluate(context).ToLocal(&retValue))
throw std::runtime_error("Failure loading module");
resolver->Resolve(context, module->GetModuleNamespace()).FromMaybe(false);
} catch(const std::exception& err) {
std::string errmsg(std::string("problem loading module ") + *name + ": " + err.what());
resolver->Reject(context, ToJSString(errmsg.c_str())).FromMaybe(false);
} catch(...) {
resolver->Reject(context, ToJSString("Unknown failure loading dynamic module")).FromMaybe(false);
}
return promise;
}

static v8::MaybeLocal<v8::Promise> ResolveDynamicModuleCallback(
v8::Local<v8::Context> context,
#if V8_VERSION_TOTAL >= 908
v8::Local<v8::Data> host_defined_options,
v8::Local<v8::Value> resource_name,
#else
v8::Local<v8::ScriptOrModule> referrer,
#endif
v8::Local<v8::String> specifier
FixedArrayParam
) {
return dynamic_module_loader(context, specifier);
}

static v8::ScriptOrigin make_origin(std::string filename){
#if defined(ISNODEJS) && NODEJS_LTS_API < 18
return v8::ScriptOrigin(ToJSString( filename.c_str()), v8::Integer::New(isolate, 0),
v8::Integer::New(isolate, 0), v8::False(isolate), v8::Local<v8::Integer>(),
v8::Local<v8::Value>(), v8::False(isolate), v8::False(isolate), v8::True(isolate));
#elif V8_VERSION_TOTAL < 1201
return v8::ScriptOrigin(isolate,ToJSString( filename.c_str()), 0, 0, false, -1,
v8::Local<v8::Value>(), false, false, true);
#else
return v8::ScriptOrigin(ToJSString( filename.c_str()), 0, 0, false, -1,
v8::Local<v8::Value>(), false, false, true);
#endif
}

/* Helper fun that compiles JavaScript source code */
static v8::Local<v8::Module> read_module(std::string filename, v8::Local<v8::Context> context){
v8::Local<v8::String> source_text = ToJSString(read_text(filename).c_str());
if(source_text.IsEmpty())
throw std::runtime_error("Failed to load JavaScript source. Check memory/stack limits.");
v8::ScriptCompiler::Source source(source_text, make_origin(filename));
v8::Local<v8::Module> module;
if (!v8::ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module))
throw std::runtime_error("Failed to run CompileModule() source.");
if(!module->InstantiateModule(context, ResolveModuleCallback).FromMaybe(false))
throw std::runtime_error("Failed to run InstantiateModule().");
return module;
}


// [[Rcpp::init]]
void start_v8_isolate(void *dll){
#ifdef V8_ICU_DATA_PATH
Expand Down Expand Up @@ -103,6 +188,7 @@ void start_v8_isolate(void *dll){
uintptr_t CurrentStackPosition = reinterpret_cast<uintptr_t>(__builtin_frame_address(0));
isolate->SetStackLimit(CurrentStackPosition - kWorkerMaxStackSize);
#endif
isolate->SetHostImportModuleDynamicallyCallback(ResolveDynamicModuleCallback);
}

/* Helper fun that compiles JavaScript source code */
Expand Down
5 changes: 5 additions & 0 deletions tests/testthat/modules/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
async function run_test(){
const {foo, bar} = await import("modules/my-module.mjs");
return foo + bar();
}

2 changes: 2 additions & 0 deletions tests/testthat/modules/my-dependency.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const a = 123;
export const b = 456;
6 changes: 6 additions & 0 deletions tests/testthat/modules/my-module.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// my-module.js
import { a,b } from 'modules/my-dependency.mjs';
export const foo = a;
export function bar(){
return b;
}
8 changes: 8 additions & 0 deletions tests/testthat/test_modules.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
context("ESM modules")

test_that("get modules", {
ctx <- V8::v8()
ctx$source('modules/main.js')
out <- ctx$eval('run_test()', await = TRUE)
expect_equal(out, "579")
})

0 comments on commit 32be402

Please sign in to comment.