summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-08-14 16:38:14 +0200
committerFlorian Dold <florian.dold@gmail.com>2019-08-14 16:38:14 +0200
commit66eb46b1a40c088b1096ba2f294796404b1a530c (patch)
tree785f6d979885157f4987972cf7246004c5c2cb9d
parentda736d8259331a8ef13bf4bbb10bbb8a5c0e5299 (diff)
downloadakono-66eb46b1a40c088b1096ba2f294796404b1a530c.tar.gz
akono-66eb46b1a40c088b1096ba2f294796404b1a530c.tar.bz2
akono-66eb46b1a40c088b1096ba2f294796404b1a530c.zip
messaging and module loading
-rw-r--r--.gitmodules4
-rw-r--r--.idea/inspectionProfiles/Project_Default.xml6
-rw-r--r--.idea/vcs.xml33
-rw-r--r--deps/.gitignore2
m---------deps/android-node-v80
-rwxr-xr-xdeps/build_v847
-rw-r--r--library/src/androidTest/java/akono/InstrumentedAkonoTests.kt79
-rw-r--r--library/src/main/cpp/CMakeLists.txt8
-rw-r--r--library/src/main/cpp/akono-jni.cpp400
-rw-r--r--library/src/main/java/akono/AkoniJni.kt39
-rw-r--r--library/src/main/java/akono/AkonoJni.kt208
11 files changed, 655 insertions, 171 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..c99a6ae7
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "deps/android-node-v8"]
+ path = deps/android-node-v8
+ url = https://git.taler.net/android-node-v8.git
+ branch = androidbuild
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..769fc1d4
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+ <profile version="1.0">
+ <option name="myName" value="Project Default" />
+ <inspection_tool class="JniGetMethod" enabled="false" level="WARNING" enabled_by_default="false" />
+ </profile>
+</component> \ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 67327d03..62bd7a01 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -3,38 +3,5 @@
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/_bad_scm/v8/third_party/jinja2_OEiwf/jinja2" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/_bad_scm/v8/third_party/markupsafeC1dcbW/markupsafe" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/_depot_tools" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/build" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/buildtools" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/buildtools/clang_format/script" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/buildtools/third_party/libc++/trunk" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/buildtools/third_party/libc++abi/trunk" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/buildtools/third_party/libunwind/trunk" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/test/wasm-js" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/test/wasm-js/data" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/third_party/depot_tools" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/third_party/icu" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/third_party/jinja2" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/third_party/markupsafe" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/third_party/perfetto" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/third_party/proguard" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/third_party/protobuf" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/tools/clang" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node/deps/v8/tools/gyp" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/_depot_tools" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/build" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/buildtools" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/buildtools/clang_format/script" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/buildtools/third_party/libc++/trunk" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/buildtools/third_party/libc++abi/trunk" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/buildtools/third_party/libunwind/trunk" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/test/wasm-js" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/third_party/depot_tools" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/third_party/icu" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/third_party/proguard" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/tools/clang" vcs="Git" />
- <mapping directory="$PROJECT_DIR$/../deps/node_old/deps/v8/tools/gyp" vcs="Git" />
</component>
</project> \ No newline at end of file
diff --git a/deps/.gitignore b/deps/.gitignore
index fb17e1f5..e3ca6473 100644
--- a/deps/.gitignore
+++ b/deps/.gitignore
@@ -1 +1 @@
-android-toolchain/
+compiled
diff --git a/deps/android-node-v8 b/deps/android-node-v8
new file mode 160000
+Subproject 7c040c7d01621f0f704fd88481e8dd455247f04
diff --git a/deps/build_v8 b/deps/build_v8
deleted file mode 100755
index 2c8b780b..00000000
--- a/deps/build_v8
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/usr/bin/env bash
-
-set -eu -o pipefail
-
-scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
-cd $scriptdir
-
-NDK=$1
-
-DEST_CPU_ARCH='arm'
-API_LEVEL=26
-BUILD_HOST_TAG='linux-x86_64'
-TOOLCHAIN_DIR=$NDK/toolchains/llvm/prebuilt/$BUILD_HOST_TAG/
-
-if [[ ! -d "$NDK" ]]; then
- echo "Android NDK directory '$NDK' invalid"
- exit 1
-fi
-
-ln -fs $NDK ./node/deps/v8/third_party/android_ndk
-
-# We do not export anything about the tool chain, as v8
-# will pick their own toolchain for android from the ndk
-
-cd ./node/deps/v8
-
-python2 ./tools/node/fetch_deps.py $PWD
-
-mode=debug
-
-./tools/dev/v8gen.py arm.$mode -- \
- 'target_os="android"' \
- 'target_cpu="arm"' \
- 'is_component_build=true' \
- 'v8_android_log_stdout=true' \
- 'v8_use_external_startup_data=false' \
- 'v8_use_snapshot=true' \
- 'v8_enable_debugging_features=true' \
- 'v8_enable_embedded_builtins=true' \
- 'is_clang=true'
-
-./_depot_tools/ninja -C out.gn/arm.$mode v8 d8
-
-cd $scriptdir
-x=compiled/armeabi-v7a/
-mkdir -p $x
-cp node/deps/v8/out.gn/arm.$mode/*.so $x/
diff --git a/library/src/androidTest/java/akono/InstrumentedAkonoTests.kt b/library/src/androidTest/java/akono/InstrumentedAkonoTests.kt
index b2e8e92a..beda5119 100644
--- a/library/src/androidTest/java/akono/InstrumentedAkonoTests.kt
+++ b/library/src/androidTest/java/akono/InstrumentedAkonoTests.kt
@@ -3,11 +3,42 @@ package akono.test;
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
import org.junit.Test
-import androidx.test.filters.SmallTest
import androidx.test.filters.LargeTest
-import org.junit.Assert.assertTrue
import org.junit.Assert.assertEquals
import akono.AkonoJni
+import akono.ModuleResult
+import android.util.Log
+import java.util.concurrent.LinkedBlockingDeque
+
+
+class SyncMessageHandler : AkonoJni.MessageHandler {
+ private val messageQueue = LinkedBlockingDeque<String>()
+ override fun handleMessage(message: String) {
+ messageQueue.put(message)
+ }
+
+ fun waitForMessage(): String {
+ return messageQueue.take()
+ }
+}
+
+
+class StaticModuleLoadHandler : AkonoJni.LoadModuleHandler {
+ private val modules: MutableMap<String, String> = HashMap()
+
+ override fun loadModule(name: String, paths: Array<String>): ModuleResult? {
+ val code = modules.get(name) ?: return null
+ if (modules.containsKey(name)) {
+ return ModuleResult("/vmodroot/$name.js", code)
+ }
+ return null
+ }
+
+ fun registerModule(name: String, source: String) {
+ modules[name] = source
+ }
+}
+
// @RunWith is required only if you use a mix of JUnit3 and JUnit4.
@RunWith(AndroidJUnit4::class)
@@ -16,15 +47,39 @@ public class InstrumentedAkonoTestOne {
@Test
fun myJsTest() {
val ajni: AkonoJni = AkonoJni()
- assertEquals("2", ajni.evalJs("1+1"))
- assertEquals("36", ajni.evalJs("6*6"))
- assertEquals("42", ajni.evalJs("(()=>{let x = 42; return x;})()"))
- //assertEquals(null, ajni.evalJs("throw Error('hello exc')"))
- //assertEquals(null, ajni.evalJs("undefinedX + undefinedY"))
- //assertEquals("123", ajni.evalJs("console.log('hello world'); 123;"))
- //assertEquals("123", ajni.evalJs("require"))
-
- assertEquals("undefined", ajni.evalJs("const myVal = 42"))
- assertEquals("43", ajni.evalJs("myVal + 1"))
+ assertEquals("2", ajni.evalSimpleJs("1+1"))
+ assertEquals("36", ajni.evalSimpleJs("6*6"))
+ assertEquals("42", ajni.evalSimpleJs("(()=>{let x = 42; return x;})()"))
+ assertEquals("undefined", ajni.evalSimpleJs("const myVal = 42"))
+ assertEquals("43", ajni.evalSimpleJs("myVal + 1"))
+
+ val myHandler = SyncMessageHandler()
+ ajni.setMessageHandler(myHandler)
+ ajni.evalNodeCode("console.log('hi from the test case')")
+ // Tell the message handler to just ping back messages to us
+ ajni.evalNodeCode("global.__akono_onMessage = (x) => { global.__akono_sendMessage(x); }")
+ val sentMessage = "Hello AKONO!!"
+ ajni.sendMessage(sentMessage)
+ val receivedMessage = myHandler.waitForMessage()
+ assertEquals(sentMessage, receivedMessage)
+ Log.i("myapp", "test case received message: $receivedMessage")
+
+ val myModHandler = StaticModuleLoadHandler()
+
+ ajni.setLoadModuleHandler(myModHandler)
+
+ myModHandler.registerModule("a", """
+ |console.log('I am module a');
+ |exports.foo = () => { global.__akono_sendMessage('hello42'); };
+ """.trimMargin())
+
+ ajni.evalNodeCode("a = require('a');")
+ ajni.evalNodeCode("a.foo()")
+
+ val msg2 = myHandler.waitForMessage()
+
+ assertEquals("hello42", msg2)
+
+ ajni.waitStopped()
}
}
diff --git a/library/src/main/cpp/CMakeLists.txt b/library/src/main/cpp/CMakeLists.txt
index 72259215..10d6396f 100644
--- a/library/src/main/cpp/CMakeLists.txt
+++ b/library/src/main/cpp/CMakeLists.txt
@@ -11,12 +11,12 @@ if(NOT EXISTS ${deps_dir})
message( FATAL_ERROR "Dependency directory does not exist")
endif()
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++14")
include_directories(
- ${deps_dir}/node/src
- ${deps_dir}/node/deps/v8/include
- ${deps_dir}/node/deps/uv/include
+ ${deps_dir}/android-node-v8/src
+ ${deps_dir}/android-node-v8/deps/v8/include
+ ${deps_dir}/android-node-v8/deps/uv/include
)
add_library(node SHARED IMPORTED)
diff --git a/library/src/main/cpp/akono-jni.cpp b/library/src/main/cpp/akono-jni.cpp
index f355362b..b9e71012 100644
--- a/library/src/main/cpp/akono-jni.cpp
+++ b/library/src/main/cpp/akono-jni.cpp
@@ -2,12 +2,35 @@
#include <jni.h>
#include <libplatform/libplatform.h>
#include <v8.h>
+
#define NODE_WANT_INTERNALS 1
+
#include <node.h>
+
#include <uv.h>
#include <unistd.h>
#include <android/log.h>
+#include <node_main_instance.h>
+#include <node_native_module_env.h>
+
+
+
+// Provide stubs so that libnode.so links properly
+namespace node {
+ namespace native_module {
+ const bool has_code_cache = false;
+
+ void NativeModuleEnv::InitializeCodeCache() {}
+ }
+ v8::StartupData *NodeMainInstance::GetEmbeddedSnapshotBlob() {
+ return nullptr;
+ }
+
+ const std::vector<size_t> *NodeMainInstance::GetIsolateDataIndexes() {
+ return nullptr;
+ }
+}
static int pfd[2];
@@ -15,25 +38,22 @@ static pthread_t thr;
static const char *tag = "myapp";
-static void *thread_func(void*)
-{
+static void *thread_func(void *) {
ssize_t rdsz;
char buf[128];
- while((rdsz = read(pfd[0], buf, sizeof buf - 1)) > 0) {
- if(buf[rdsz - 1] == '\n') --rdsz;
+ while ((rdsz = read(pfd[0], buf, sizeof buf - 1)) > 0) {
+ if (buf[rdsz - 1] == '\n') --rdsz;
buf[rdsz] = 0; /* add null-terminator */
__android_log_write(ANDROID_LOG_DEBUG, tag, buf);
}
return 0;
}
-static void mylog(const char *msg)
-{
+static void mylog(const char *msg) {
__android_log_write(ANDROID_LOG_DEBUG, tag, msg);
}
-int start_logger(const char *app_name)
-{
+int start_logger(const char *app_name) {
tag = app_name;
/* make stdout line-buffered and stderr unbuffered */
@@ -46,14 +66,13 @@ int start_logger(const char *app_name)
dup2(pfd[1], 2);
/* spawn the logging thread */
- if(pthread_create(&thr, 0, thread_func, 0) == -1)
+ if (pthread_create(&thr, 0, thread_func, 0) == -1)
return -1;
pthread_detach(thr);
return 0;
}
-
/**
* Helper class to manage conversion from a JNI string to a C string.
*/
@@ -61,14 +80,16 @@ class JStringValue {
private:
jstring m_jstr;
const char *m_cstr;
- JNIEnv* m_env;
+ JNIEnv *m_env;
public:
- JStringValue(JNIEnv* env, jstring s) : m_env(env), m_jstr(s) {
+ JStringValue(JNIEnv *env, jstring s) : m_env(env), m_jstr(s) {
m_cstr = env->GetStringUTFChars(s, NULL);
}
+
~JStringValue() {
m_env->ReleaseStringUTFChars(m_jstr, m_cstr);
}
+
const char *operator*() {
return m_cstr;
}
@@ -86,6 +107,58 @@ static void InitNode(std::vector<const char *> argv) {
node::Init(&ret_argc, &argv[0], &ret_exec_argc, &ret_exec_argv);
}
+// Forward declarations
+void notifyCb(uv_async_t *async);
+
+static void sendMessageCallback(const v8::FunctionCallbackInfo<v8::Value> &args);
+static void loadModuleCallback(const v8::FunctionCallbackInfo<v8::Value> &args);
+static void getDataCallback(const v8::FunctionCallbackInfo<v8::Value> &args);
+
+static const char *main_code = "global.__akono_run = (x) => {"
+ " console.log('running code', x);"
+ " global.eval(x);"
+ "};"
+ ""
+ "global.__akono_onMessage = (x) => {"
+ " console.log('got __akono_onMessage', x);"
+ "};"
+ ""
+ "mod = require('module');"
+ "mod._saved_findPath = mod._findPath;"
+ "mod._akonoMods = {};"
+ "mod._findPath = (request, paths, isMain) => {"
+ " console.log('in _findPath');"
+ " const res = mod._saved_findPath(request, paths, isMain);"
+ " if (res !== false) return res;"
+ " const args = JSON.stringify({ request, paths});"
+ " const loadResult = JSON.parse(global.__akono_loadModule(args));"
+ " console.log('got loadModule result', loadResult);"
+ " if (!loadResult) return false;"
+ " mod._akonoMods[loadResult.path] = loadResult;"
+ " console.log('returning path', loadResult.path);"
+ " return loadResult.path;"
+ "};"
+ ""
+ "function stripBOM(content) {"
+ " if (content.charCodeAt(0) === 0xFEFF) {"
+ " content = content.slice(1);"
+ " }"
+ " return content;"
+ "}"
+ ""
+ "mod._saved_js_extension = mod._extensions[\".js\"];"
+ "mod._extensions[\".js\"] = (module, filename) => {"
+ " console.log('handling js extension', [module, filename]);"
+ " if (mod._akonoMods.hasOwnProperty(filename)) {"
+ " const akmod = mod._akonoMods[filename];"
+ " console.log('found mod', akmod);"
+ " const content = akmod.content;"
+ " return module._compile(stripBOM(content), filename);"
+ " }"
+ " console.log('falling back');"
+ " return mod._saved_js_extension(module, filename);"
+ "};";
+
class NativeAkonoInstance {
private:
@@ -97,8 +170,16 @@ public:
v8::Isolate *isolate;
node::Environment *environment;
v8::Persistent<v8::Context> globalContext;
+ uv_async_t async_notify;
+ uv_loop_t *loop;
+ bool breakRequested = false;
+ JNIEnv *currentJniEnv = nullptr;
+ jobject currentJniThiz = nullptr;
NativeAkonoInstance() : globalContext() {
+ loop = uv_default_loop();
+ uv_async_init(loop, &async_notify, notifyCb);
+ async_notify.data = this;
if (!logInitialized) {
start_logger("myapp");
@@ -112,14 +193,13 @@ public:
// Here, only the arguments used to initialize the global node/v8 platform
// are relevant, the others are skipped.
- InitNode(std::vector<const char *>{"node", "--trace-events-enabled"});
+ InitNode(std::vector<const char *>{"node", "-e", main_code});
platform = node::InitializeV8Platform(10);
v8::V8::Initialize();
v8Initialized = true;
}
-
node::ArrayBufferAllocator *allocator = node::CreateArrayBufferAllocator();
this->isolate = node::NewIsolate(allocator, uv_default_loop());
@@ -128,18 +208,20 @@ public:
v8::HandleScope handle_scope(isolate);
node::IsolateData *isolateData = node::CreateIsolateData(
- this->isolate,
- uv_default_loop(),
- platform,
- allocator);
+ this->isolate,
+ uv_default_loop(),
+ platform,
+ allocator);
- globalContext.Reset(isolate, v8::Context::New(isolate));
+ globalContext.Reset(isolate, node::NewContext(isolate));
+
+ v8::Local<v8::Context> context = globalContext.Get(isolate);
// Arguments for node itself
- std::vector<const char*> nodeArgv{"node", "-e", "console.log('hello world');"};
+ std::vector<const char *> nodeArgv{"node", "-e", "console.log('hello world');"};
// Arguments for the script run by node
- std::vector<const char*> nodeExecArgv{};
+ std::vector<const char *> nodeExecArgv{};
mylog("entering global scopt");
@@ -155,25 +237,86 @@ public:
nodeExecArgv.size(),
&nodeExecArgv[0]);
+
mylog("loading environment");
node::LoadEnvironment(environment);
- mylog("finished environment");
+ mylog("finished loading environment");
+
+ v8::Local<v8::ObjectTemplate> dataTemplate = v8::ObjectTemplate::New(isolate);
+ dataTemplate->SetInternalFieldCount(1);
+ v8::Local<v8::Object> dataObject = dataTemplate->NewInstance(context).ToLocalChecked();
+ dataObject->SetAlignedPointerInInternalField(0, this);
+
+ v8::Local<v8::Function> sendMessageFunction = v8::Function::New(context,
+ sendMessageCallback,
+ dataObject).ToLocalChecked();
+
+ v8::Local<v8::Function> loadModuleFunction = v8::Function::New(context,
+ loadModuleCallback,
+ dataObject).ToLocalChecked();
+
+ v8::Local<v8::Function> getDataFunction = v8::Function::New(context,
+ getDataCallback,
+ dataObject).ToLocalChecked();
+
+ v8::Local<v8::Object> global = context->Global();
+
+ global->Set(v8::String::NewFromUtf8(isolate, "__akono_sendMessage",
+ v8::NewStringType::kNormal).ToLocalChecked(),
+ sendMessageFunction);
+
+ global->Set(v8::String::NewFromUtf8(isolate, "__akono_loadModule",
+ v8::NewStringType::kNormal).ToLocalChecked(),
+ loadModuleFunction);
+
+ // Get data synchronously (!) from the embedder
+ global->Set(v8::String::NewFromUtf8(isolate, "__akono_getData",
+ v8::NewStringType::kNormal).ToLocalChecked(),
+ getDataFunction);
- //v8::Isolate::CreateParams create_params;
- //create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
- //this->isolate = v8::Isolate::New(create_params);
-//
-// node::IsolateData *isolateData = node::CreateIsolateData()
-// node::Environment *environment = node::CreateEnvironment(isolateData);
+ }
+
+ /**
+ * Process the node message loop until a break has been requested.
+ *
+ * @param env JNI env of the thread we're running in.
+ */
+ void runNode() {
+ this->breakRequested = false;
+ while (1) {
+ uv_run(uv_default_loop(), UV_RUN_ONCE);
+ if (this->breakRequested)
+ break;
+ }
+ }
+
+ /**
+ * Inject code into the running node instance.
+ *
+ * Must not be called from a different thread.
+ */
+ void makeCallback(const char *code) {
+ mylog("in makeCallback");
+ v8::Isolate::Scope isolate_scope(isolate);
+ v8::HandleScope handle_scope(isolate);
+ v8::Local<v8::Context> context = globalContext.Get(isolate);
+ v8::Context::Scope context_scope(context);
+ v8::Local<v8::Object> global = context->Global();
+ v8::Local<v8::Value> argv[] = {
+ v8::String::NewFromUtf8(isolate, code,
+ v8::NewStringType::kNormal).ToLocalChecked()
+ };
+ mylog("calling node::MakeCallback");
+ node::MakeCallback(isolate, global, "__akono_run", 1, argv, {0, 0});
}
~NativeAkonoInstance() {
//this->isolate->Dispose();
}
- jstring evalJs(JNIEnv* env, jstring sourceString) {
+ jstring evalJs(JNIEnv *env, jstring sourceString) {
mylog("begin evalJs");
JStringValue jsv(env, sourceString);
@@ -234,25 +377,212 @@ bool NativeAkonoInstance::logInitialized = false;
node::MultiIsolatePlatform *NativeAkonoInstance::platform = nullptr;
+void notifyCb(uv_async_t *async) {
+ NativeAkonoInstance *akono = (NativeAkonoInstance *) async->data;
+ mylog("async notifyCb called!");
+ akono->breakRequested = true;
+}
+
+static void sendMessageCallback(const v8::FunctionCallbackInfo<v8::Value> &args) {
+ if (args.Length() < 1) return;
+ v8::Isolate *isolate = args.GetIsolate();
+ v8::HandleScope scope(isolate);
+ v8::Local<v8::Value> arg = args[0];
+ v8::String::Utf8Value value(isolate, arg);
+ mylog("sendMessageCallback called, yay!");
+
+ v8::Local<v8::Object> data = v8::Local<v8::Object>::Cast(args.Data());
+
+ mylog("getting instance");
+ NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0);
+
+ JNIEnv *env = myInstance->currentJniEnv;
+
+ if (env == nullptr) {
+ mylog("FATAL: JNI env is nullptr");
+ return;
+ }
+
+ mylog("finding class");
+ jclass clazz = env->FindClass("akono/AkonoJni");
+
+ if (clazz == nullptr) {
+ mylog("FATAL: class not found");
+ return;
+ }
+
+ mylog("creating strings");
+ jstring jstr1 = env->NewStringUTF("message");
+ jstring jstr2 = env->NewStringUTF(*value);
+
+ mylog("getting method");
+
+ jmethodID meth = env->GetMethodID(clazz, "internalOnNotify", "(Ljava/lang/String;Ljava/lang/String;)V");
+
+ if (meth == nullptr) {
+ mylog("FATAL: method not found");
+ return;
+ }
+
+ mylog("calling method");
+
+ env->CallVoidMethod(myInstance->currentJniThiz, meth, jstr1, jstr2);
+}
+
+
+static void loadModuleCallback(const v8::FunctionCallbackInfo<v8::Value> &args) {
+ if (args.Length() < 1) return;
+ v8::Isolate *isolate = args.GetIsolate();
+ v8::HandleScope scope(isolate);
+ v8::Local<v8::Value> arg = args[0];
+ v8::String::Utf8Value value(isolate, arg);
+ mylog("sendMessageCallback called, yay!");
+
+ v8::Local<v8::Object> data = v8::Local<v8::Object>::Cast(args.Data());
+
+ mylog("getting instance");
+ NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0);
+
+ JNIEnv *env = myInstance->currentJniEnv;
+
+ if (env == nullptr) {
+ mylog("FATAL: JNI env is nullptr");
+ return;
+ }
+
+ mylog("finding class");
+ jclass clazz = env->FindClass("akono/AkonoJni");
+
+ if (clazz == nullptr) {
+ mylog("FATAL: class not found");
+ return;
+ }
+
+ mylog("creating strings");
+ jstring jstr1 = env->NewStringUTF(*value);
+
+ mylog("getting method");
+
+ jmethodID meth = env->GetMethodID(clazz, "internalOnModuleLoad", "(Ljava/lang/String;)Ljava/lang/String;");
+
+ if (meth == nullptr) {
+ mylog("FATAL: method not found");
+ return;
+ }
+
+ mylog("calling method");
+
+ jstring jresult = (jstring) env->CallObjectMethod(myInstance->currentJniThiz, meth, jstr1);
+
+ JStringValue resultStringValue(env, jresult);
+
+ printf("before creating string, res %s\n", *resultStringValue);
+
+ // Create a string containing the JavaScript source code.
+ v8::Local<v8::String> rs =
+ v8::String::NewFromUtf8(isolate, *resultStringValue,
+ v8::NewStringType::kNormal)
+ .ToLocalChecked();
+
+ args.GetReturnValue().Set(rs);
+}
+
+
+static void getDataCallback(const v8::FunctionCallbackInfo<v8::Value> &args) {
+ if (args.Length() < 1) return;
+ v8::Isolate *isolate = args.GetIsolate();
+ v8::HandleScope scope(isolate);
+ v8::Local<v8::Value> arg = args[0];
+ v8::String::Utf8Value value(isolate, arg);
+ mylog("getDataCallback called");
+
+ v8::Local<v8::Object> data = v8::Local<v8::Object>::Cast(args.Data());
+ mylog("getting instance");
+ NativeAkonoInstance *myInstance = (NativeAkonoInstance *) data->GetAlignedPointerFromInternalField(0);
+
+ JNIEnv *env = myInstance->currentJniEnv;
+ if (env == nullptr) {
+ mylog("FATAL: JNI env is nullptr");
+ return;
+ }
+
+ mylog("finding class");
+ jclass clazz = env->FindClass("akono/AkonoJni");
+
+ if (clazz == nullptr) {
+ mylog("FATAL: class not found");
+ return;
+ }
+
+ mylog("creating strings");
+ jstring jstr1 = env->NewStringUTF(*value);
+
+ mylog("getting method");
+
+ jmethodID meth = env->GetMethodID(clazz, "internalOnGetData", "(Ljava/lang/String;)Ljava/lang/String;");
+
+ if (meth == nullptr) {
+ mylog("FATAL: method not found");
+ return;
+ }
+
+ mylog("calling method");
+
+ jstring jresult = (jstring) env->CallObjectMethod(myInstance->currentJniThiz, meth, jstr1);
+
+ JStringValue resultStringValue(env, jresult);
+
+ printf("before creating string, res %s\n", *resultStringValue);
+
+ // Create a string containing the JavaScript source code.
+ v8::Local<v8::String> rs =
+ v8::String::NewFromUtf8(isolate, *resultStringValue,
+ v8::NewStringType::kNormal)
+ .ToLocalChecked();
+
+ args.GetReturnValue().Set(rs);
+}
+
+
extern "C" JNIEXPORT jobject JNICALL
-Java_akono_AkonoJni_initNative(JNIEnv* env, jobject thiz)
-{
+Java_akono_AkonoJni_initNative(JNIEnv *env, jobject thiz) {
NativeAkonoInstance *myInstance = new NativeAkonoInstance();
return env->NewDirectByteBuffer(myInstance, 0);
}
extern "C" JNIEXPORT void JNICALL
-Java_akono_AkonoJni_destroyNative(JNIEnv* env, jobject thiz, jobject buf)
-{
+Java_akono_AkonoJni_destroyNative(JNIEnv *env, jobject thiz, jobject buf) {
NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf);
delete myInstance;
}
extern "C" JNIEXPORT jstring JNICALL
-Java_akono_AkonoJni_evalJs(JNIEnv* env, jobject thiz, jstring sourceStr, jobject buf)
-{
+Java_akono_AkonoJni_evalJs(JNIEnv *env, jobject thiz, jstring sourceStr, jobject buf) {
NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf);
return myInstance->evalJs(env, sourceStr);
}
+
+extern "C" JNIEXPORT void JNICALL
+Java_akono_AkonoJni_notifyNative(JNIEnv *env, jobject thiz, jobject buf) {
+ NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf);
+ uv_async_send(&myInstance->async_notify);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_akono_AkonoJni_runNode(JNIEnv *env, jobject thiz, jobject buf) {
+ NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf);
+ myInstance->currentJniEnv = env;
+ myInstance->currentJniThiz = thiz;
+ myInstance->runNode();
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_akono_AkonoJni_makeCallbackNative(JNIEnv *env, jobject thiz, jstring sourceStr, jobject buf) {
+ JStringValue jsv(env, sourceStr);
+ NativeAkonoInstance *myInstance = (NativeAkonoInstance *) env->GetDirectBufferAddress(buf);
+ myInstance->currentJniEnv = env;
+ myInstance->currentJniThiz = thiz;
+ return myInstance->makeCallback(*jsv);
+}
diff --git a/library/src/main/java/akono/AkoniJni.kt b/library/src/main/java/akono/AkoniJni.kt
deleted file mode 100644
index 2210a7ef..00000000
--- a/library/src/main/java/akono/AkoniJni.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package akono
-
-import java.nio.ByteBuffer
-
-typealias AkonoNativePointer = ByteBuffer
-
-class AkonoJni {
- external fun stringFromJNI(): String
-
- private external fun evalJs(source: String, p: AkonoNativePointer): String
-
- private external fun destroyNative(b: AkonoNativePointer)
- private external fun initNative(nodeArgv: Array<out String>): AkonoNativePointer
-
- private external fun runNodeLoop(b: AkonoNativePointer)
-
- private external fun postMessageToNode(message: String, b: AkonoNativePointer)
-
- private external fun waitForMessageFromNode(b: AkonoNativePointer): String
-
- private var internalNativePointer: AkonoNativePointer
-
- fun evalJs(source: String): String = evalJs(source, internalNativePointer)
-
- @Override
- protected fun finalize() {
- destroyNative(internalNativePointer)
- }
-
- constructor(vararg nodeArgv: String) {
- internalNativePointer = initNative(nodeArgv)
- }
-
- companion object {
- init {
- System.loadLibrary("akono-jni")
- }
- }
-} \ No newline at end of file
diff --git a/library/src/main/java/akono/AkonoJni.kt b/library/src/main/java/akono/AkonoJni.kt
new file mode 100644
index 00000000..4a89a3f6
--- /dev/null
+++ b/library/src/main/java/akono/AkonoJni.kt
@@ -0,0 +1,208 @@
+package akono
+
+import android.util.Base64
+import android.util.Log
+import org.json.JSONObject
+import java.lang.Exception
+import java.nio.ByteBuffer
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.LinkedBlockingDeque
+import kotlin.concurrent.thread
+
+typealias AkonoNativePointer = ByteBuffer
+
+data class ModuleResult(val path: String, val contents: String)
+
+class AkonoJni(vararg nodeArgv: String) {
+ private var getDataHandler: GetDataHandler? = null
+ private var messageHandler: MessageHandler? = null
+ private var loadModuleHandler: LoadModuleHandler? = null
+ private val initializedLatch = CountDownLatch(1)
+
+ private val workQueue = LinkedBlockingDeque<() -> Unit>()
+
+ private external fun evalJs(source: String, p: AkonoNativePointer): String
+ private external fun runNode(p: AkonoNativePointer)
+
+ private external fun makeCallbackNative(source: String, p: AkonoNativePointer)
+
+ private external fun destroyNative(b: AkonoNativePointer)
+ private external fun initNative(nodeArgv: Array<out String>): AkonoNativePointer
+ private external fun notifyNative(b: AkonoNativePointer)
+
+ private lateinit var internalNativePointer: AkonoNativePointer
+
+ private val jniThread: Thread
+
+ private var stopped = false
+
+ /**
+ * Schedule a block do be executed in the node thread.
+ */
+ private fun scheduleNodeThread(b: () -> Unit) {
+ initializedLatch.await()
+ workQueue.put(b)
+ notifyNative()
+ }
+
+ /**
+ * Called by node/v8 from its thread.
+ */
+ @Suppress("unused")
+ private fun internalOnNotify(type: String, payload: String) {
+ Log.i("myapp", "internalOnNotify called")
+ Log.i("myapp", "type: $type")
+ Log.i("myapp", "payload: $payload")
+ messageHandler?.handleMessage(payload)
+ }
+
+ /**
+ * Called by node/v8 from its thread.
+ */
+ @Suppress("unused")
+ private fun internalOnModuleLoad(loadInfoStr: String): String {
+ Log.i("myapp", "internalOnModuleLoad called")
+ Log.i("myapp", "loadInfoStr is $loadInfoStr")
+ try {
+ val loadInfo = JSONObject(loadInfoStr)
+ val request: String = loadInfo.getString("request")
+ Log.i("myapp", "request is $request")
+ val handler = loadModuleHandler
+ if (handler != null) {
+ val modResult = handler.loadModule(request, arrayOf()) ?: return "null"
+ val result = JSONObject()
+ result.put("path", modResult.path)
+ result.put("content", modResult.contents)
+ return result.toString()
+ } else {
+ Log.v("myapp", "no module load handler registered")
+ return "null"
+ }
+ } catch (e: Exception) {
+ Log.v("myapp", "exception during internalOnModuleLoad: $e")
+ return "null"
+ }
+ }
+
+ /**
+ * Called by node/v8 from its thread.
+ */
+ @Suppress("unused")
+ private fun internalOnGetData(what: String): String? {
+ Log.i("myapp", "internalOnGetData called for $what")
+ val data = getDataHandler?.handleGetData(what) ?: return null
+ return Base64.encodeToString(data, Base64.NO_WRAP)
+ }
+
+
+ fun notifyNative() {
+ initializedLatch.await()
+ notifyNative(internalNativePointer)
+ }
+
+ /**
+ * Schedule Node.JS to be run.
+ */
+ fun evalSimpleJs(source: String): String {
+ val latch = CountDownLatch(1)
+ var result: String? = null
+ scheduleNodeThread {
+ result = evalJs(source, internalNativePointer)
+ latch.countDown()
+ }
+ latch.await()
+ return result ?: throw Exception("invariant failed")
+ }
+
+ fun evalNodeCode(source: String) {
+ scheduleNodeThread {
+ makeCallbackNative(source, internalNativePointer)
+ }
+ }
+
+ /**
+ * Send a message to node, calling global.__akono_onMessage.
+ */
+ fun sendMessage(message: String) {
+ val encoded = Base64.encodeToString(message.toByteArray(), Base64.NO_WRAP)
+ val source = """
+ if (global.__akono_onMessage) {
+ const msg = (new Buffer('$encoded', 'base64')).toString('ascii');
+ global.__akono_onMessage(msg);
+ } else {
+ console.log("WARN: no __akono_onMessage defined");
+ }
+ """.trimIndent()
+ evalNodeCode(source)
+ }
+
+ /**
+ *
+ */
+ fun waitStopped(): Unit {
+ Log.i("myapp", "waiting for stop")
+ scheduleNodeThread {
+ stopped = true
+ }
+ jniThread.join()
+ return
+ }
+
+ /**
+ * Register a message handler that is called when the JavaScript code
+ * running in [runNodeJs] calls __akono_sendMessage
+ *
+ * Does not block.
+ */
+ fun setMessageHandler(handler: MessageHandler) {
+ this.messageHandler = handler
+ }
+
+ fun setLoadModuleHandler(handler: LoadModuleHandler) {
+ this.loadModuleHandler = handler
+ }
+
+ fun setGetDataHandler(handler: GetDataHandler) {
+ this.getDataHandler = handler
+ }
+
+ @Override
+ protected fun finalize() {
+ destroyNative(internalNativePointer)
+ }
+
+ init {
+ jniThread = thread {
+ internalNativePointer = initNative(nodeArgv)
+ initializedLatch.countDown()
+ while (true) {
+ runNode(internalNativePointer)
+ while (true) {
+ val w = workQueue.poll() ?: break
+ w()
+ }
+ if (stopped) {
+ break
+ }
+ }
+ }
+ }
+
+ companion object {
+ init {
+ System.loadLibrary("akono-jni")
+ }
+ }
+
+ interface MessageHandler {
+ fun handleMessage(message: String)
+ }
+
+ interface LoadModuleHandler {
+ fun loadModule(name: String, paths: Array<String>): ModuleResult?
+ }
+
+ interface GetDataHandler {
+ fun handleGetData(what: String): ByteArray?
+ }
+} \ No newline at end of file