diff --git a/.vscode/settings.json b/.vscode/settings.json index 4f16494..2ca504c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,15 @@ "jvalue", "Ljava", "varargs" - ] + ], + "files.associations": { + "new": "cpp", + "queue": "cpp", + "array": "cpp", + "deque": "cpp", + "list": "cpp", + "vector": "cpp", + "string_view": "cpp", + "typeinfo": "cpp" + } } \ No newline at end of file diff --git a/binding.gyp b/binding.gyp index 5629c01..edf6931 100644 --- a/binding.gyp +++ b/binding.gyp @@ -34,6 +34,7 @@ 'src/javaObject.cpp', 'src/javaScope.cpp', 'src/methodCallBaton.cpp', + 'src/javaGlobalData.cpp', 'src/nodeJavaBridge.cpp', 'src/utils.cpp' ], diff --git a/lib/nodeJavaBridge.js b/lib/nodeJavaBridge.js index cfbbfec..5421546 100644 --- a/lib/nodeJavaBridge.js +++ b/lib/nodeJavaBridge.js @@ -20,6 +20,7 @@ if (!binaryPath) { const bindings = require(binaryPath); const java = (module.exports = new bindings.Java()); + java.classpath.push(path.resolve(__dirname, "../commons-lang3-node-java.jar")); java.classpath.push(path.resolve(__dirname, __dirname, "../src-java")); java.classpath.pushDir = function (dir) { diff --git a/package-lock.json b/package-lock.json index a596ce1..98d2463 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@eslint/js": "^9.27.0", "chalk": "2.4.2", "eslint": "^9.27.0", + "find-root": "^1.1.0", "globals": "^16.1.0", "prettier": "^3.5.3", "vitest": "^3.1.3", @@ -1970,6 +1971,13 @@ "winreg": "~1.2.2" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true, + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", diff --git a/package.json b/package.json index 1d20298..c740414 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@eslint/js": "^9.27.0", "chalk": "2.4.2", "eslint": "^9.27.0", + "find-root": "^1.1.0", "globals": "^16.1.0", "prettier": "^3.5.3", "vitest": "^3.1.3", diff --git a/src/java.cpp b/src/java.cpp index 17743cc..0705fb5 100644 --- a/src/java.cpp +++ b/src/java.cpp @@ -15,22 +15,6 @@ #define DYNAMIC_PROXY_JS_ERROR -4 -#ifdef WIN32 -typedef long threadId; -#else -typedef pthread_t threadId; -#endif - -threadId v8ThreadId; -bool isDefaultLoopRunning = false; - -std::queue queue_dynamicProxyJsCallData; -uv_mutex_t uvMutex_dynamicProxyJsCall; -uv_async_t uvAsync_dynamicProxyJsCall; - -/*static*/ Nan::Persistent Java::s_ct; -/*static*/ std::string Java::s_nativeBindingLocation; - void my_sleep(int dur) { #ifdef WIN32 Sleep(dur); @@ -59,15 +43,23 @@ void EIO_CallJs(DynamicProxyJsCallData *callData); void uvAsyncCb_dynamicProxyJsCall(uv_async_t *handle) { DynamicProxyJsCallData *callData; + v8::Isolate *isolate = v8::Isolate::GetCurrent(); + if (isolate == nullptr) { + return; + } + JavaGlobalData *data = Nan::GetIsolateData(isolate); + if (data == nullptr) { + return; + } do { - uv_mutex_lock(&uvMutex_dynamicProxyJsCall); - if (!queue_dynamicProxyJsCallData.empty()) { - callData = queue_dynamicProxyJsCallData.front(); - queue_dynamicProxyJsCallData.pop(); + uv_mutex_lock(&data->uvMutex_dynamicProxyJsCall); + if (!data->queue_dynamicProxyJsCallData.empty()) { + callData = data->queue_dynamicProxyJsCallData.front(); + data->queue_dynamicProxyJsCallData.pop(); } else { callData = NULL; } - uv_mutex_unlock(&uvMutex_dynamicProxyJsCall); + uv_mutex_unlock(&data->uvMutex_dynamicProxyJsCall); if (callData) { EIO_CallJs(callData); @@ -75,16 +67,16 @@ void uvAsyncCb_dynamicProxyJsCall(uv_async_t *handle) { } while (callData); } -/*static*/ void Java::Init(v8::Local target) { +/*static*/ void Java::Init(v8::Local target, JavaGlobalData *data) { Nan::HandleScope scope; - v8ThreadId = my_getThreadId(); - isDefaultLoopRunning = false; // init as false + data->v8ThreadId = my_getThreadId(); + data->isDefaultLoopRunning = false; // init as false - uv_mutex_init(&uvMutex_dynamicProxyJsCall); + uv_mutex_init(&data->uvMutex_dynamicProxyJsCall); v8::Local t = Nan::New(New); - s_ct.Reset(t); + data->ct.Reset(t); t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(Nan::New("Java").ToLocalChecked()); @@ -116,13 +108,21 @@ void uvAsyncCb_dynamicProxyJsCall(uv_async_t *handle) { NAN_METHOD(Java::New) { Nan::HandleScope scope; + v8::Isolate *isolate = v8::Isolate::GetCurrent(); + if (isolate == nullptr) { + return; + } + JavaGlobalData *data = Nan::GetIsolateData(isolate); + if (data == nullptr) { + return; + } - if (!isDefaultLoopRunning) { - uv_async_init(uv_default_loop(), &uvAsync_dynamicProxyJsCall, uvAsyncCb_dynamicProxyJsCall); - isDefaultLoopRunning = true; + if (!data->isDefaultLoopRunning) { + uv_async_init(uv_default_loop(), &data->uvAsync_dynamicProxyJsCall, uvAsyncCb_dynamicProxyJsCall); + data->isDefaultLoopRunning = true; } - Java *self = new Java(); + Java *self = new Java(data); self->Wrap(info.This()); Nan::Set(self->handle(), Nan::New("classpath").ToLocalChecked(), Nan::New()); @@ -134,10 +134,10 @@ NAN_METHOD(Java::New) { info.GetReturnValue().Set(info.This()); } -Java::Java() { - this->m_jvm = NULL; - this->m_env = NULL; - +Java::Java(JavaGlobalData *data) { + m_data = data; + m_jvm = NULL; + m_env = NULL; m_SyncSuffix = "Sync"; m_AsyncSuffix = ""; doSync = true; @@ -255,7 +255,7 @@ v8::Local Java::createJVM(JavaVM **jvm, JNIEnv **env) { Nan::Get(this->handle(), Nan::New("nativeBindingLocation").ToLocalChecked()) .FromMaybe(v8::Local()); Nan::Utf8String nativeBindingLocationStr(v8NativeBindingLocation); - s_nativeBindingLocation = *nativeBindingLocationStr; + m_data->nativeBindingLocation = *nativeBindingLocationStr; // get other options v8::Local optionsValue = @@ -347,7 +347,7 @@ NAN_GETTER(Java::AccessorProhibitsOverwritingGetter) { info.GetReturnValue().Set(Nan::New(self->m_optionsArray)); return; } else if (!strcmp("nativeBindingLocation", *nameStr)) { - info.GetReturnValue().Set(Nan::New(Java::s_nativeBindingLocation.c_str()).ToLocalChecked()); + info.GetReturnValue().Set(Nan::New(self->m_data->nativeBindingLocation.c_str()).ToLocalChecked()); return; } else if (!strcmp("asyncOptions", *nameStr)) { info.GetReturnValue().Set(Nan::New(self->m_asyncOptions)); @@ -519,8 +519,8 @@ NAN_METHOD(Java::newProxy) { // find constructor jclass objectClazz = env->FindClass("java/lang/Object"); jobjectArray methodArgs = env->NewObjectArray(2, objectClazz, NULL); - env->SetObjectArrayElement(methodArgs, 0, - v8ToJava(env, Nan::New(s_nativeBindingLocation.c_str()).ToLocalChecked())); + env->SetObjectArrayElement( + methodArgs, 0, v8ToJava(env, Nan::New(self->m_data->nativeBindingLocation.c_str()).ToLocalChecked())); env->SetObjectArrayElement(methodArgs, 1, longToJavaLongObj(env, (jlong)dynamicProxyData)); jobject method = javaFindConstructor(env, clazz, methodArgs); if (method == NULL) { @@ -1268,8 +1268,9 @@ NAN_METHOD(Java::instanceOf) { } NAN_METHOD(Java::stop) { - if (isDefaultLoopRunning) { - uv_close((uv_handle_t *)&uvAsync_dynamicProxyJsCall, NULL); + Java *self = Nan::ObjectWrap::Unwrap(info.This()); + if (self->m_data->isDefaultLoopRunning) { + uv_close((uv_handle_t *)&self->m_data->uvAsync_dynamicProxyJsCall, NULL); } } @@ -1382,6 +1383,8 @@ JNIEXPORT jobject JNICALL Java_node_NodeDynamicProxyClass_callJs(JNIEnv *env, jo DynamicProxyData *dynamicProxyData = (DynamicProxyData *)ptr; + JavaGlobalData *data = dynamicProxyData->java->getData(); + // args needs to be global, you can't send env across thread boundaries DynamicProxyJsCallData callData; callData.dynamicProxyData = dynamicProxyData; @@ -1396,7 +1399,7 @@ JNIEXPORT jobject JNICALL Java_node_NodeDynamicProxyClass_callJs(JNIEnv *env, jo callData.methodName = javaObjectToString(env, env->CallObjectMethod(method, method_getName)); assertNoException(env); - if (v8ThreadIdEquals(myThreadId, v8ThreadId)) { + if (v8ThreadIdEquals(myThreadId, data->v8ThreadId)) { EIO_CallJs(&callData); } else { if (args) { @@ -1405,10 +1408,10 @@ JNIEXPORT jobject JNICALL Java_node_NodeDynamicProxyClass_callJs(JNIEnv *env, jo hasArgsGlobalRef = true; } - uv_mutex_lock(&uvMutex_dynamicProxyJsCall); - queue_dynamicProxyJsCallData.push(&callData); // we wait for work to finish, so ok to pass ref to local var - uv_mutex_unlock(&uvMutex_dynamicProxyJsCall); - uv_async_send(&uvAsync_dynamicProxyJsCall); + uv_mutex_lock(&data->uvMutex_dynamicProxyJsCall); + data->queue_dynamicProxyJsCallData.push(&callData); // we wait for work to finish, so ok to pass ref to local var + uv_mutex_unlock(&data->uvMutex_dynamicProxyJsCall); + uv_async_send(&data->uvAsync_dynamicProxyJsCall); while (!callData.done) { my_sleep(100); diff --git a/src/java.h b/src/java.h index 83c5cfc..761f3aa 100644 --- a/src/java.h +++ b/src/java.h @@ -2,6 +2,7 @@ #ifndef _node_java_h_ #define _node_java_h_ +#include "javaGlobalData.h" #include #include #include @@ -16,14 +17,14 @@ class Java : public Nan::ObjectWrap { public: - static void Init(v8::Local target); + static void Init(v8::Local target, JavaGlobalData *data); JavaVM *getJvm() { return m_jvm; } JNIEnv *getJavaEnv() { return m_env; } // can only be used safely by the main thread as this is the thread it belongs to jobject getClassLoader() { return m_classLoader; } + JavaGlobalData *getData() { return m_data; } -public: bool DoSync() const { return doSync; } bool DoAsync() const { return doAsync; } bool DoPromise() const { return doPromise; } @@ -32,7 +33,7 @@ class Java : public Nan::ObjectWrap { std::string PromiseSuffix() const { return m_PromiseSuffix; } private: - Java(); + Java(JavaGlobalData *data); ~Java(); v8::Local createJVM(JavaVM **jvm, JNIEnv **env); void destroyJVM(JavaVM **jvm, JNIEnv **env); @@ -63,12 +64,11 @@ class Java : public Nan::ObjectWrap { static NAN_SETTER(AccessorProhibitsOverwritingSetter); v8::Local ensureJvm(); - static Nan::Persistent s_ct; + JavaGlobalData *m_data; JavaVM *m_jvm; JNIEnv *m_env; // can only be used safely by the main thread as this is the thread it belongs to jobject m_classLoader; std::string m_classPath; - static std::string s_nativeBindingLocation; Nan::Persistent m_classPathArray; Nan::Persistent m_optionsArray; Nan::Persistent m_asyncOptions; diff --git a/src/javaGlobalData.cpp b/src/javaGlobalData.cpp new file mode 100644 index 0000000..d5b2302 --- /dev/null +++ b/src/javaGlobalData.cpp @@ -0,0 +1 @@ +#include "javaGlobalData.h" diff --git a/src/javaGlobalData.h b/src/javaGlobalData.h new file mode 100644 index 0000000..139dfd0 --- /dev/null +++ b/src/javaGlobalData.h @@ -0,0 +1,38 @@ +#ifndef _javaGlobalData_h_ +#define _javaGlobalData_h_ + +#include +#include +#include +#include +#include +#include +#include "utils.h" + +#ifdef WIN32 +typedef long threadId; +#else +typedef pthread_t threadId; +#endif + +class JavaGlobalData { +public: + threadId v8ThreadId; + bool isDefaultLoopRunning = false; + + std::queue queue_dynamicProxyJsCallData; + uv_mutex_t uvMutex_dynamicProxyJsCall; + uv_async_t uvAsync_dynamicProxyJsCall; + + Nan::Persistent ct; + std::string nativeBindingLocation; + + explicit JavaGlobalData(v8::Isolate *isolate) { + // Ensure this per-addon-instance data is deleted at environment cleanup. + node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this); + } + + static void DeleteInstance(void *data) { delete static_cast(data); } +}; + +#endif diff --git a/src/nodeJavaBridge.cpp b/src/nodeJavaBridge.cpp index 55b6bb4..847842f 100644 --- a/src/nodeJavaBridge.cpp +++ b/src/nodeJavaBridge.cpp @@ -1,18 +1,22 @@ #include "java.h" +#include "javaGlobalData.h" #include "javaObject.h" -extern "C" { -static void init(v8::Local target, v8::Local, void *) { - Java::Init(target); +NAN_MODULE_INIT(init) { + v8::Isolate *isolate = v8::Isolate::GetCurrent(); + if (isolate == nullptr) { + return; + } + JavaGlobalData *data = new JavaGlobalData(isolate); + Nan::SetIsolateData(isolate, data); + + Java::Init(target, data); JavaObject::Init(target); } -NODE_MODULE(nodejavabridge_bindings, init); -} +NAN_MODULE_WORKER_ENABLED(nodejavabridge_bindings, init); #ifdef WIN32 - BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE; } - #endif diff --git a/test/web-worker.test.js b/test/web-worker.test.js new file mode 100644 index 0000000..a578a0c --- /dev/null +++ b/test/web-worker.test.js @@ -0,0 +1,29 @@ + +import { describe, expect, test } from "vitest"; +import { getJava } from "../testHelpers.js"; +import { Worker } from "node:worker_threads"; +import path from "node:path"; +import findRoot from 'find-root'; + +const java = getJava(); +const root = findRoot(__dirname); + +describe('web-worker', () => { + test('run java in a web worker', async () => { + await new Promise((resolve, reject) => { + const version = java.callStaticMethodSync("java.lang.System", "getProperty", "java.version"); + expect(version).toBeTruthy(); + + const worker = new Worker(path.join(root, 'test/web-worker.worker.js')); + worker.on('message', (result) => { + expect(result).toBe(42); + resolve(); + }); + worker.on('error', reject); + worker.on('messageerror', () => reject(new Error('message error'))); + worker.on('exit', () => reject(new Error('exit'))); + + worker.postMessage({ name: 'getJavaVersion' }); + }); + }); +}); diff --git a/test/web-worker.worker.js b/test/web-worker.worker.js new file mode 100644 index 0000000..f57954b --- /dev/null +++ b/test/web-worker.worker.js @@ -0,0 +1,20 @@ +import { parentPort } from 'node:worker_threads'; +import { getJava } from "../testHelpers.js"; + +parentPort.on('message', (message) => { + console.log('message.name', message.name); + switch (message.name) { + case 'getJavaVersion': + getJavaVersion(); + break; + default: + console.error('message', message); + parentPort.postMessage(`unhandled name: ${message.name}`); + } +}); + +function getJavaVersion() { + console.log('a'); + const version = getJava().callStaticMethodSync("java.lang.System", "getProperty", "java.version"); + parentPort.postMessage(version); +} diff --git a/testHelpers.js b/testHelpers.js index 93e8fa0..7d73299 100644 --- a/testHelpers.js +++ b/testHelpers.js @@ -1,4 +1,4 @@ -export const java = require("./"); +export const java = require("."); java.options.push("-Djava.awt.headless=true"); //java.options.push('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005');