diff --git a/README.md b/README.md index 00e2b7b..668e2b6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ cordova plugin add nodejs-mobile-cordova ## Reporting Issues -We have a [central repo](https://github.com/janeasystems/nodejs-mobile/issues) where we manage all the issues related to Node.js for Mobile Apps, including specific issues of the Node.js for Mobile Cordova plugin. +We have a [central repo](https://github.com/janeasystems/nodejs-mobile/issues) where we manage all the issues related to Node.js for Mobile Apps, including specific issues of the Node.js for Mobile Apps Cordova plugin. So please, open the issue [there](https://github.com/janeasystems/nodejs-mobile/issues). ## Cordova Methods @@ -32,26 +32,52 @@ So please, open the issue [there](https://github.com/janeasystems/nodejs-mobile/ ### nodejs.start ```js - nodejs.start(scriptFileName, callback); + nodejs.start(scriptFileName, callback [, options]); ``` +| Param | Type | +| --- | --- | +| scriptFileName | string | +| callback | function | +| options | [StartupOptions](#cordova.StartupOptions) | + ### nodejs.startWithScript ```js - nodejs.startWithScript(scriptBody, callback); + nodejs.startWithScript(scriptBody, callback [, options]); ``` +| Param | Type | +| --- | --- | +| scriptBody | string | +| callback | function | +| options | [StartupOptions](#cordova.StartupOptions) | ### nodejs.channel.setListener ```js nodejs.channel.setListener(listenerCallback); ``` +| Param | Type | +| --- | --- | +| listenerCallback | function | ### nodejs.channel.send ```js nodejs.channel.send(message); ``` +| Param | Type | +| --- | --- | +| message | string | + + +### StartupOptions: object +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| redirectOutputToLogcat | boolean | true | Allows to disable the redirection of the Node stdout/stderr to the Android logcat | + +Note: the stdout/stderr redirection is applied to the whole application, the side effect is that some undesired/duplicated output may appear in the logcat. +For example, the Chromium console output `I/chromium: [INFO:CONSOLE(xx)]` is also sent to stderr and will show up in logcat has well, with the `NODEJS-MOBILE` log tag. ## Node.js Methods @@ -67,13 +93,19 @@ So please, open the issue [there](https://github.com/janeasystems/nodejs-mobile/ ``` cordova.channel.send(message); ``` +| Param | Type | +| --- | --- | +| message | string | ### cordova.channel.on ``` cordova.channel.on('message', listnerCallback); ``` - +| Param | Type | +| --- | --- | +| 'message' | const string | +| listnerCallback | function | ## Usage @@ -111,8 +143,7 @@ Switch to Xcode: * enlarge the `Console` area, at the end of the console log it should show: ``` -2017-10-02 18:49:18.606100+0200 HelloCordova[2182:1463518] NodeJs Engine Started +2017-10-02 18:49:18.606100+0200 HelloCordova[2182:1463518] Node.js Mobile Engine Started [node] received: Hello from Cordova! 2017-10-02 18:49:18.690132+0200 HelloCordova[2182:1463518] [cordova] received: Replying to this message: Hello from Cordova! ``` - diff --git a/install/hooks/ios/after-plugin-install.js b/install/hooks/ios/after-plugin-install.js index b529e20..0ccaf02 100644 --- a/install/hooks/ios/after-plugin-install.js +++ b/install/hooks/ios/after-plugin-install.js @@ -15,8 +15,8 @@ module.exports = function(context) { xcodeProject.parseSync(); var firstTargetUUID = xcodeProject.getFirstTarget().uuid; - //Adds a build phase to rebuild native modules - var rebuildNativeModulesBuildPhaseName = 'Build NodeJS Mobile Native Modules'; + // Adds a build phase to rebuild native modules. + var rebuildNativeModulesBuildPhaseName = 'Build Node.js Mobile Native Modules'; var rebuildNativeModulesBuildPhaseScript = ` set -e if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then @@ -55,8 +55,8 @@ popd ); } - //Adds a build phase to sign native modules - var signNativeModulesBuildPhaseName = 'Sign NodeJS Mobile Native Modules'; + // Adds a build phase to sign native modules. + var signNativeModulesBuildPhaseName = 'Sign Node.js Mobile Native Modules'; var signNativeModulesBuildPhaseScript = ` set -e if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then diff --git a/install/hooks/ios/before-plugin-uninstall.js b/install/hooks/ios/before-plugin-uninstall.js index db297cb..a2a1666 100644 --- a/install/hooks/ios/before-plugin-uninstall.js +++ b/install/hooks/ios/before-plugin-uninstall.js @@ -4,7 +4,7 @@ var fs = require('fs'); module.exports = function(context) { var xcode = context.requireCordovaModule('xcode'); - //Adds a custom function to remove script build phases, which is not supported on cordova's Xcode module yet. + // Adds a custom function to remove script build phases, which is not supported on cordova's Xcode module yet. xcode.project.prototype.myRemovePbxScriptBuildPhase = function (buildPhaseName, target) { var buildPhaseTargetUuid = target || this.getFirstTarget().uuid; @@ -14,7 +14,7 @@ module.exports = function(context) { throw new Error("Couldn't find the build script phase to remove: " + buildPhaseName ); } - // remove the '_comment' suffix to get the actual uuid + // Remove the '_comment' suffix to get the actual uuid. var buildPhaseUuid=buildPhaseUuid_comment.split('_')[0]; // Remove from the pbxBuildPhaseObjects @@ -49,15 +49,15 @@ module.exports = function(context) { xcodeProject.parseSync(); var firstTargetUUID = xcodeProject.getFirstTarget().uuid; - //Removes the build phase to rebuild native modules - var rebuildNativeModulesBuildPhaseName = 'Build NodeJS Mobile Native Modules'; + // Removes the build phase to rebuild native modules. + var rebuildNativeModulesBuildPhaseName = 'Build Node.js Mobile Native Modules'; var rebuildNativeModulesBuildPhase = xcodeProject.buildPhaseObject('PBXShellScriptBuildPhase', rebuildNativeModulesBuildPhaseName, firstTargetUUID); if (rebuildNativeModulesBuildPhase) { xcodeProject.myRemovePbxScriptBuildPhase(rebuildNativeModulesBuildPhaseName, firstTargetUUID); } - //Removes the build phase to sign native modules - var signNativeModulesBuildPhaseName = 'Sign NodeJS Mobile Native Modules'; + // Removes the build phase to sign native modules. + var signNativeModulesBuildPhaseName = 'Sign Node.js Mobile Native Modules'; var signNativeModulesBuildPhase = xcodeProject.buildPhaseObject('PBXShellScriptBuildPhase', signNativeModulesBuildPhaseName, firstTargetUUID); if (signNativeModulesBuildPhase) { xcodeProject.myRemovePbxScriptBuildPhase(signNativeModulesBuildPhaseName, firstTargetUUID); diff --git a/install/nodejs-mobile-cordova-assets/builtin_modules/cordova-bridge/package.json b/install/nodejs-mobile-cordova-assets/builtin_modules/cordova-bridge/package.json index 7fb42bb..96c08d9 100644 --- a/install/nodejs-mobile-cordova-assets/builtin_modules/cordova-bridge/package.json +++ b/install/nodejs-mobile-cordova-assets/builtin_modules/cordova-bridge/package.json @@ -1,7 +1,7 @@ { "name": "cordova-bridge", "version": "0.1.0", - "description": "NodeJS for Mobile Native Bridge", + "description": "Node.js for Mobile Apps Native Bridge", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/install/sample-project/www/js/index.js b/install/sample-project/www/js/index.js index c22189e..ddeecf4 100644 --- a/install/sample-project/www/js/index.js +++ b/install/sample-project/www/js/index.js @@ -29,18 +29,21 @@ var app = { app.initialize(); function channelListener(msg) { - console.log("[cordova] received:", msg); -} - + console.log('[cordova] received: ' + msg); +}; + +function startupCallback(err) { + if (err) { + console.log(err); + } else { + console.log ('Node.js Mobile Engine Started'); + nodejs.channel.send('Hello from Cordova!'); + } +}; + function startNodeProject() { -nodejs.channel.setListener(channelListener); -nodejs.start("main.js", - function(err) { - if (err) { - console.log(err); - } else { - console.log ("NodeJs Engine Started"); - nodejs.channel.send("Hello from Cordova!"); - } - }); -} + nodejs.channel.setListener(channelListener); + nodejs.start('main.js', startupCallback); + // To disable the stdout/stderr redirection to the Android logcat: + // nodejs.start('main.js', startupCallback, { redirectOutputToLogcat: false }); +}; diff --git a/install/sample-project/www/nodejs-project/package.json b/install/sample-project/www/nodejs-project/package.json index be300bc..f4dd2de 100644 --- a/install/sample-project/www/nodejs-project/package.json +++ b/install/sample-project/www/nodejs-project/package.json @@ -1,7 +1,7 @@ { "name": "nodejs-mobile-sample-project", "version": "0.1.0", - "description": "NodeJS for Mobile sample project", + "description": "Node.js for Mobile Apps sample project", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/plugin.xml b/plugin.xml index dcb3de1..78ed9fb 100644 --- a/plugin.xml +++ b/plugin.xml @@ -4,8 +4,8 @@ id="nodejs-mobile-cordova" version="0.1.2"> - NodeJS Mobile - NodeJS for Mobile Cordova Plugin + Node.js Mobile + Node.js for Mobile Apps Cordova Plugin MIT cordova,mobile,nodejs,node.js @@ -35,8 +35,8 @@ - - + + @@ -69,9 +69,9 @@ - - - + + + diff --git a/src/android/CMakeLists.txt b/src/android/CMakeLists.txt index 5d69248..cdb0e18 100644 --- a/src/android/CMakeLists.txt +++ b/src/android/CMakeLists.txt @@ -11,13 +11,13 @@ cmake_minimum_required(VERSION 3.4.1) # Gradle automatically packages shared libraries with your APK. add_library( # Sets the name of the library. - native-module + nodejs-mobile-cordova-native-lib # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). - native-module.cpp + native-lib.cpp cordova-bridge.cpp ) @@ -54,7 +54,7 @@ find_library( # Sets the name of the path variable. # build script, prebuilt third-party libraries, or system libraries. target_link_libraries( # Specifies the target library. - native-module + nodejs-mobile-cordova-native-lib libnode diff --git a/src/android/java/com/janeasystems/cdvnodejsmobile/NodeJS.java b/src/android/java/com/janeasystems/cdvnodejsmobile/NodeJS.java index 2ba8cb3..45f43c6 100644 --- a/src/android/java/com/janeasystems/cdvnodejsmobile/NodeJS.java +++ b/src/android/java/com/janeasystems/cdvnodejsmobile/NodeJS.java @@ -1,7 +1,15 @@ +/* + Node.js for Mobile Apps Cordova plugin. + + Implements the plugin APIs exposed to the Cordova layer and routes messages + between the Cordova layer and the Node.js engine. + */ + package com.janeasystems.cdvnodejsmobile; import org.apache.cordova.*; import org.json.JSONArray; +import org.json.JSONObject; import org.json.JSONException; import android.util.Log; @@ -37,18 +45,18 @@ public class NodeJS extends CordovaPlugin { private long previousLastUpdateTime = 0; private static boolean appPaused = false; - private static String LOGTAG = "NodeJS-Cordova"; + private static String LOGTAG = "NODEJS-CORDOVA"; private static boolean engineAlreadyStarted = false; private static CallbackContext channelListenerContext = null; static { - System.loadLibrary("native-module"); + System.loadLibrary("nodejs-mobile-cordova-native-lib"); System.loadLibrary("node"); } - public native Integer startNodeWithArguments(String[] arguments, String nodePath); + public native Integer startNodeWithArguments(String[] arguments, String nodePath, boolean redirectOutputToLogcat); public native void sendToNode(String msg); public native String getCurrentABIName(); @@ -81,10 +89,12 @@ public boolean execute(String action, JSONArray data, CallbackContext callbackCo result = this.setChannelListener(callbackContext); } else if (action.equals("startEngine")) { String target = data.getString(0); - result = this.startEngine(target, callbackContext); + JSONObject startOptions = data.getJSONObject(1); + result = this.startEngine(target, startOptions, callbackContext); } else if (action.equals("startEngineWithScript")) { String scriptBody = data.getString(0); - result = this.startEngineWithScript(scriptBody, callbackContext); + JSONObject startOptions = data.getJSONObject(1); + result = this.startEngineWithScript(scriptBody, startOptions, callbackContext); } else { Log.e(LOGTAG, "Invalid action: " + action); result = false; @@ -137,7 +147,8 @@ private boolean setChannelListener(final CallbackContext callbackContext) { return true; } - private boolean startEngine(String scriptFileName, final CallbackContext callbackContext) { + private boolean startEngine(final String scriptFileName, final JSONObject startOptions, + final CallbackContext callbackContext) { Log.v(LOGTAG, "StartEngine: " + scriptFileName); if (NodeJS.engineAlreadyStarted == true) { @@ -158,6 +169,8 @@ private boolean startEngine(String scriptFileName, final CallbackContext callbac return false; } + final boolean redirectOutputToLogcat = getOptionRedirectOutputToLogcat(startOptions); + NodeJS.engineAlreadyStarted = true; Log.v(LOGTAG, "Script absolute path: " + scriptFileAbsolutePath); new Thread(new Runnable() { @@ -165,7 +178,8 @@ private boolean startEngine(String scriptFileName, final CallbackContext callbac public void run() { startNodeWithArguments( new String[]{"node", scriptFileAbsolutePath}, - NodeJS.nodePath); + NodeJS.nodePath, + redirectOutputToLogcat); } }).start(); @@ -173,7 +187,8 @@ public void run() { return true; } - private boolean startEngineWithScript(String scriptBody, final CallbackContext callbackContext) { + private boolean startEngineWithScript(final String scriptBody, final JSONObject startOptions, + final CallbackContext callbackContext) { Log.v(LOGTAG, "StartEngineWithScript: " + scriptBody); boolean result = true; String errorMsg = ""; @@ -183,6 +198,8 @@ private boolean startEngineWithScript(String scriptBody, final CallbackContext c return false; } + final boolean redirectOutputToLogcat = getOptionRedirectOutputToLogcat(startOptions); + final String scriptBodyToRun = new String(scriptBody); Log.v(LOGTAG, "Script absolute path: " + scriptBody); new Thread(new Runnable() { @@ -190,7 +207,8 @@ private boolean startEngineWithScript(String scriptBody, final CallbackContext c public void run() { startNodeWithArguments( new String[]{"node", "-e", scriptBodyToRun}, - NodeJS.nodePath); + NodeJS.nodePath, + redirectOutputToLogcat); } }).start(); @@ -442,4 +460,28 @@ private static boolean deleteFolderRecursively(File file) { return false; } } + + private static boolean getOptionRedirectOutputToLogcat(final JSONObject startOptions) { + if (BuildConfig.DEBUG) { + if (startOptions.names() != null) { + for (int i = 0; i < startOptions.names().length(); i++) { + try { + Log.v(LOGTAG, "Start engine option: " + startOptions.names().getString(i)); + } catch (JSONException e) { + } + } + } + } + + final String OPTION_NAME = "redirectOutputToLogcat"; + boolean result = true; + if (startOptions.has(OPTION_NAME) == true) { + try { + result = startOptions.getBoolean(OPTION_NAME); + } catch(JSONException e) { + Log.e(LOGTAG, e.getMessage()); + } + } + return result; + } } \ No newline at end of file diff --git a/src/android/jni/cordova-bridge.cpp b/src/android/jni/cordova-bridge.cpp deleted file mode 100644 index 01fce66..0000000 --- a/src/android/jni/cordova-bridge.cpp +++ /dev/null @@ -1,261 +0,0 @@ -#include "include/node/node_api.h" -#include "include/node/uv.h" -#include "cordova-bridge.h" -#define NM_F_BUILTIN 0x1 -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#define warn_console(...) __android_log_print(ANDROID_LOG_WARN, "JNI", __VA_ARGS__) - -// Some helper macros from node/test/addons-napi/common.h - -// Empty value so that macros here are able to return NULL or void -#define NAPI_RETVAL_NOTHING // Intentionally blank #define - -#define GET_AND_THROW_LAST_ERROR(env) \ - do \ - { \ - const napi_extended_error_info *error_info; \ - napi_get_last_error_info((env), &error_info); \ - bool is_pending; \ - napi_is_exception_pending((env), &is_pending); \ - /* If an exception is already pending, don't rethrow it */ \ - if (!is_pending) \ - { \ - const char *error_message = error_info->error_message != NULL ? error_info->error_message : "empty error message"; \ - napi_throw_error((env), NULL, error_message); \ - } \ - } while (0) - -#define NAPI_ASSERT_BASE(env, assertion, message, ret_val) \ - do \ - { \ - if (!(assertion)) \ - { \ - napi_throw_error( \ - (env), \ - NULL, \ - "assertion (" #assertion ") failed: " message); \ - return ret_val; \ - } \ - } while (0) - -// Returns NULL on failed assertion. -// This is meant to be used inside napi_callback methods. -#define NAPI_ASSERT(env, assertion, message) \ - NAPI_ASSERT_BASE(env, assertion, message, NULL) - -// Returns empty on failed assertion. -// This is meant to be used inside functions with void return type. -#define NAPI_ASSERT_RETURN_VOID(env, assertion, message) \ - NAPI_ASSERT_BASE(env, assertion, message, NAPI_RETVAL_NOTHING) - -#define NAPI_CALL_BASE(env, the_call, ret_val) \ - do \ - { \ - if ((the_call) != napi_ok) \ - { \ - GET_AND_THROW_LAST_ERROR((env)); \ - return ret_val; \ - } \ - } while (0) - -// Returns NULL if the_call doesn't return napi_ok. -#define NAPI_CALL(env, the_call) \ - NAPI_CALL_BASE(env, the_call, NULL) - -// Returns empty if the_call doesn't return napi_ok. -#define NAPI_CALL_RETURN_VOID(env, the_call) \ - NAPI_CALL_BASE(env, the_call, NAPI_RETVAL_NOTHING) - -class QueuedFunc { - public: - QueuedFunc(napi_env &env, napi_ref &function) : env(env), function(function){}; - - void notify_message(char *s) { - napi_env original_env = env; - - napi_handle_scope scope; - napi_open_handle_scope(original_env, &scope); - - napi_ref original_function_ref = function; - - napi_value callback; - napi_get_reference_value(original_env, original_function_ref, &callback); - napi_value global; - napi_get_global(original_env, &global); - - napi_value message; - napi_create_string_utf8(original_env, s, strlen(s), &message); - - napi_value *argv = &message; - size_t argc = 1; - - napi_value result; - napi_call_function(original_env, global, callback, argc, argv, &result); - - napi_close_handle_scope(original_env, scope); - } - - private: - napi_ref function; - napi_env env; -}; - -std::map pool; -int32_t my_little_pool_incrementer = 1; - -t_bridge_callback embedder_callback = NULL; - -std::mutex queueLock; -std::queue messageQueue; -uv_async_t *queue_uv_handle = NULL; - -void RegisterBridgeCallback(t_bridge_callback callback) { - embedder_callback = callback; -} - -void close_cb(uv_handle_t *handle) { - free(((uv_async_t *)handle)->data); - free(handle); -}; - -void doRegisteredCallbacks(uv_async_t *handle) { - std::map copiedPool; - copiedPool = pool; - char *message = (char *)(handle->data); - std::map::iterator it; - for (it = copiedPool.begin(); it != copiedPool.end(); it++) { - it->second->notify_message(message); - } - uv_close((uv_handle_t *)handle, close_cb); -} - -void flushMessageQueue(uv_async_t *handle) { - char *message; - queueLock.lock(); - bool has_elements = !messageQueue.empty(); - queueLock.unlock(); - while (has_elements) { - queueLock.lock(); - message = messageQueue.front(); - messageQueue.pop(); - has_elements = !messageQueue.empty(); - queueLock.unlock(); - - uv_async_t *handle = (uv_async_t *)malloc(sizeof(uv_async_t)); - uv_async_init(uv_default_loop(), handle, doRegisteredCallbacks); - handle->data = (void *)message; - uv_async_send(handle); - } -} - -void init_queue_uv_handle() { - queue_uv_handle = (uv_async_t *)malloc(sizeof(uv_async_t)); - uv_async_init(uv_default_loop(), queue_uv_handle, flushMessageQueue); - uv_async_send(queue_uv_handle); -} - -napi_value Method_RegisterListener(napi_env env, napi_callback_info info) { - if (queue_uv_handle == NULL) { - init_queue_uv_handle(); - } - size_t argc = 1; - napi_value args[1]; - NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); - - NAPI_ASSERT(env, argc == 1, "Wrong number of arguments"); - - napi_value listener_function = args[0]; - - napi_valuetype valuetype0; - NAPI_CALL(env, napi_typeof(env, listener_function, &valuetype0)); - - NAPI_ASSERT(env, valuetype0 == napi_function, "Expected a function"); - - napi_ref ref_to_function; - NAPI_CALL(env, napi_create_reference(env, listener_function, 1, &ref_to_function)); - - napi_value result; - NAPI_CALL(env, napi_create_int32(env, my_little_pool_incrementer, &result)); - - QueuedFunc *af = new QueuedFunc(env, ref_to_function); - pool[my_little_pool_incrementer++] = af; - - return result; -} - -// Let's make something appear on native code. -napi_value Method_SendMessage(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - - NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); - - NAPI_ASSERT(env, argc == 1, "Wrong number of arguments"); - - napi_value value_to_log = args[0]; - - napi_valuetype valuetype0; - NAPI_CALL(env, napi_typeof(env, value_to_log, &valuetype0)); - - if (valuetype0 != napi_string) { - NAPI_CALL(env, napi_coerce_to_string(env, value_to_log, &value_to_log)); - } - - size_t length; - size_t copied; - NAPI_CALL(env, napi_get_value_string_utf8(env, value_to_log, NULL, 0, &length)); - - // C++ cleans it automatically. - std::unique_ptr unique_buffer(new char[length + 1]()); - char *buff = unique_buffer.get(); - - NAPI_CALL(env, napi_get_value_string_utf8(env, value_to_log, buff, length + 1, &copied)); - - NAPI_ASSERT(env, copied == length, "Couldn't fully copy the message"); - - NAPI_ASSERT(env, embedder_callback, "No callback is set in native code to receive the message"); - - if (embedder_callback) { - embedder_callback(buff); - } - - return nullptr; -} - -#define DECLARE_NAPI_METHOD(name, func) { name, 0, func, 0, 0, 0, napi_default, 0 } - -napi_value Init(napi_env env, napi_value exports) { - napi_status status; - napi_property_descriptor properties[] = { - DECLARE_NAPI_METHOD("send", Method_SendMessage), - DECLARE_NAPI_METHOD("setListener", Method_RegisterListener), - }; - NAPI_CALL(env, napi_define_properties(env, exports, sizeof(properties) / sizeof(*properties), properties)); - return exports; -} - -void SendToBridge(const char *message) { - size_t messageLength = strlen(message); - char *messageCopy = (char *)calloc(sizeof(char), messageLength + 1); - - strncpy(messageCopy, message, messageLength); - - queueLock.lock(); - messageQueue.push(messageCopy); - queueLock.unlock(); - - if (queue_uv_handle != NULL) { - uv_async_send(queue_uv_handle); - } -} - -NAPI_MODULE_X(cordova_bridge, Init, NULL, NM_F_BUILTIN) diff --git a/src/android/jni/cordova-bridge.h b/src/android/jni/cordova-bridge.h deleted file mode 100644 index fa1ac1f..0000000 --- a/src/android/jni/cordova-bridge.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef CORDOVA_BRIDGE_H_ -#define CORDOVA_BRIDGE_H_ - -typedef void (*t_bridge_callback)(char* arg); -void RegisterBridgeCallback(t_bridge_callback); -void SendToBridge(const char *message); - -#endif diff --git a/src/android/jni/native-lib.cpp b/src/android/jni/native-lib.cpp new file mode 100644 index 0000000..9d71acf --- /dev/null +++ b/src/android/jni/native-lib.cpp @@ -0,0 +1,195 @@ +/* + Node.js for Mobile Apps Cordova plugin. + + The JNI layer between the Cordova plugin Java code and the Node.js bridge. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "include/node/node.h" +#include "cordova-bridge.h" + +const char *ADBTAG = "NODEJS-MOBILE"; + +// Forward declaration. +int start_redirecting_stdout_stderr(); + +// Cache the environment variable for the thread running node to call into Java. +JNIEnv* cacheEnvPointer = NULL; + +extern "C" +JNIEXPORT void JNICALL +Java_com_janeasystems_cdvnodejsmobile_NodeJS_sendToNode( + JNIEnv *env, + jobject /* this */, + jstring msg) { + const char* nativeMessage = env->GetStringUTFChars(msg, 0); + SendToNode(nativeMessage); + env->ReleaseStringUTFChars(msg, nativeMessage); +} + +extern "C" int callintoNode(int argc, char *argv[]){ + const int exit_code = node::Start(argc,argv); + return exit_code; +} + +#if defined(__arm__) + #define CURRENT_ABI_NAME "armeabi-v7a" +#elif defined(__aarch64__) + #define CURRENT_ABI_NAME "arm64-v8a" +#elif defined(__i386__) + #define CURRENT_ABI_NAME "x86" +#elif defined(__x86_64__) + #define CURRENT_ABI_NAME "x86_64" +#else + #error "Trying to compile for an unknown ABI." +#endif + +extern "C" +JNIEXPORT jstring JNICALL +Java_com_janeasystems_cdvnodejsmobile_NodeJS_getCurrentABIName( + JNIEnv *env, + jobject /* this */) { + return env->NewStringUTF(CURRENT_ABI_NAME); +} + +void rcv_message_from_node(const char* msg) { + JNIEnv *env = cacheEnvPointer; + if (!env) { + return; + } + // Try to find the class. + jclass cls2 = env->FindClass("com/janeasystems/cdvnodejsmobile/NodeJS"); + if (cls2 != nullptr) { + // Find the method. + jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "sendMessageToCordova", "(Ljava/lang/String;)V"); + if (m_sendMessage != nullptr) { + jstring java_msg=env->NewStringUTF(msg); + // Call the method. + env->CallStaticVoidMethod(cls2, m_sendMessage, java_msg); + } + } +} + +extern "C" jint JNICALL +Java_com_janeasystems_cdvnodejsmobile_NodeJS_startNodeWithArguments( + JNIEnv *env, + jobject /* this */, + jobjectArray arguments, + jstring nodePath, + jboolean redirectOutputToLogcat) { + + // Node's libuv requires all arguments being on contiguous memory. + const char* path_path = env->GetStringUTFChars(nodePath, 0); + setenv("NODE_PATH", path_path, 1); + env->ReleaseStringUTFChars(nodePath, path_path); + + // argc to pass to Node. + jsize argument_count = env->GetArrayLength(arguments); + + // Compute byte size need for all arguments in contiguous memory. + int c_arguments_size = 0; + for (int i = 0; i < argument_count ; i++) { + c_arguments_size += strlen(env->GetStringUTFChars((jstring)env->GetObjectArrayElement(arguments, i), 0)); + c_arguments_size++; // for '\0' + } + + // Stores arguments in contiguous memory. + char* args_buffer = (char*)calloc(c_arguments_size, sizeof(char)); + + // argv to pass into node. + char* argv[argument_count]; + + // To iterate through the expected start position of each argument in args_buffer. + char* current_args_position = args_buffer; + + // Populate the args_buffer and argv. + for (int i = 0; i < argument_count ; i++) { + const char* current_argument = env->GetStringUTFChars((jstring)env->GetObjectArrayElement(arguments, i), 0); + + // Copy current argument to its expected position in args_buffer. + strncpy(current_args_position, current_argument, strlen(current_argument)); + + // Save current argument start position in argv. + argv[i] = current_args_position; + + // Increment to the next argument's expected position. + current_args_position += strlen(current_args_position)+1; + } + + if (redirectOutputToLogcat == true) { + // Start threads to show stdout and stderr in logcat. + if (start_redirecting_stdout_stderr() == -1) { + __android_log_write(ANDROID_LOG_ERROR, ADBTAG, "Couldn't start redirecting stdout and stderr to logcat."); + } + } + + RegisterBridgeCallback(&rcv_message_from_node); + + cacheEnvPointer = env; + + // Start node, with argc and argv. + return jint(callintoNode(argument_count, argv)); +} + +/** + * Redirect stdout and staderr to Android's logcat + */ + +int pipe_stdout[2]; +int pipe_stderr[2]; +pthread_t thread_stdout; +pthread_t thread_stderr; + +void redirect(int pipe, int log_level) { + ssize_t redirect_size; + char buf[2048]; + while ((redirect_size = read(pipe, buf, sizeof buf - 1)) > 0) { + // __android_log_write will add a new line anyway. + if (buf[redirect_size - 1] == '\n') { + --redirect_size; + } + buf[redirect_size] = 0; + __android_log_write(log_level, ADBTAG, buf); + } +} + +void* thread_stderr_func(void*) { + redirect(pipe_stderr[0], ANDROID_LOG_ERROR); + return 0; +} + +void* thread_stdout_func(void*) { + redirect(pipe_stdout[0], ANDROID_LOG_INFO); + return 0; +} + +int start_redirecting_stdout_stderr() { + // Set stdout as unbuffered. + setvbuf(stdout, 0, _IONBF, 0); + pipe(pipe_stdout); + dup2(pipe_stdout[1], STDOUT_FILENO); + + // Set stderr as unbuffered. + setvbuf(stderr, 0, _IONBF, 0); + pipe(pipe_stderr); + dup2(pipe_stderr[1], STDERR_FILENO); + + if (pthread_create(&thread_stdout, 0, thread_stdout_func, 0) == -1) { + return -1; + } + pthread_detach(thread_stdout); + + if(pthread_create(&thread_stderr, 0, thread_stderr_func, 0) == -1) { + return -1; + } + pthread_detach(thread_stderr); + + return 0; +} \ No newline at end of file diff --git a/src/android/jni/native-module.cpp b/src/android/jni/native-module.cpp deleted file mode 100644 index 615b264..0000000 --- a/src/android/jni/native-module.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#include -#include -#include -#include - -#include "include/node/node.h" -#include "cordova-bridge.h" - -#include -#include -#include -#define warn_console(...) __android_log_print(ANDROID_LOG_WARN, "JNI", __VA_ARGS__) - -// cache the environment variable for the thread running node to call into java -JNIEnv* cacheEnvPointer = NULL; - -extern "C" -JNIEXPORT void JNICALL -Java_com_janeasystems_cdvnodejsmobile_NodeJS_sendToNode( - JNIEnv *env, - jobject /* this */, - jstring msg) { - const char* nativeMessage = env->GetStringUTFChars(msg, 0); - SendToBridge(nativeMessage); - env->ReleaseStringUTFChars(msg, nativeMessage); -} - -extern "C" int callintoNode(int argc, char *argv[]) -{ - const int exit_code = node::Start(argc,argv); - return exit_code; -} - -#if defined(__arm__) - #define CURRENT_ABI_NAME "armeabi-v7a" -#elif defined(__aarch64__) - #define CURRENT_ABI_NAME "arm64-v8a" -#elif defined(__i386__) - #define CURRENT_ABI_NAME "x86" -#elif defined(__x86_64__) - #define CURRENT_ABI_NAME "x86_64" -#else - #error "Trying to compile for an unknown ABI." -#endif - -extern "C" -JNIEXPORT jstring JNICALL -Java_com_janeasystems_cdvnodejsmobile_NodeJS_getCurrentABIName( - JNIEnv *env, - jobject /* this */) { - return env->NewStringUTF(CURRENT_ABI_NAME); -} - -#define APPNAME "CORDOVABRIDGE" - -void rcv_message_from_node(char* msg) { - JNIEnv *env = cacheEnvPointer; - if (!env) { - return; - } - // Try to find the class - jclass cls2 = env->FindClass("com/janeasystems/cdvnodejsmobile/NodeJS"); - if (cls2 != nullptr) { - // Find method - jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "sendMessageToCordova", "(Ljava/lang/String;)V"); - if (m_sendMessage != nullptr) { - jstring java_msg=env->NewStringUTF(msg); - // Call method - env->CallStaticVoidMethod(cls2, m_sendMessage, java_msg); - } - } -} - -//node's libUV requires all arguments being on contiguous memory. -extern "C" jint JNICALL -Java_com_janeasystems_cdvnodejsmobile_NodeJS_startNodeWithArguments( - JNIEnv *env, - jobject /* this */, - jobjectArray arguments, - jstring nodePath) { - - const char* path_path = env->GetStringUTFChars(nodePath, 0); - setenv("NODE_PATH", path_path, 1); - env->ReleaseStringUTFChars(nodePath, path_path); - - // argc - jsize argument_count = env->GetArrayLength(arguments); - - // Compute byte size need for all arguments in contiguous memory. - int c_arguments_size = 0; - for (int i = 0; i < argument_count ; i++) { - c_arguments_size += strlen(env->GetStringUTFChars((jstring)env->GetObjectArrayElement(arguments, i), 0)); - c_arguments_size++; // for '\0' - } - - // Stores arguments in contiguous memory. - char* args_buffer = (char*)calloc(c_arguments_size, sizeof(char)); - - // argv to pass into node. - char* argv[argument_count]; - - // To iterate through the expected start position of each argument in args_buffer. - char* current_args_position = args_buffer; - - // Populate the args_buffer and argv. - for (int i = 0; i < argument_count ; i++) - { - const char* current_argument = env->GetStringUTFChars((jstring)env->GetObjectArrayElement(arguments, i), 0); - - // Copy current argument to its expected position in args_buffer - strncpy(current_args_position, current_argument, strlen(current_argument)); - - // Save current argument start position in argv - argv[i] = current_args_position; - - // Increment to the next argument's expected position. - current_args_position += strlen(current_args_position)+1; - } - - RegisterBridgeCallback(&rcv_message_from_node); - - cacheEnvPointer = env; - - // Start node, with argc and argv. - return jint(callintoNode(argument_count, argv)); -} diff --git a/src/ios/NativeModule.cpp b/src/common/cordova-bridge/cordova-bridge.cpp similarity index 91% rename from src/ios/NativeModule.cpp rename to src/common/cordova-bridge/cordova-bridge.cpp index ad0735b..8ee801d 100644 --- a/src/ios/NativeModule.cpp +++ b/src/common/cordova-bridge/cordova-bridge.cpp @@ -1,6 +1,12 @@ -#include "NativeModule.hpp" +/* + Node.js for Mobile Apps Cordova plugin. + + Implements the bridge APIs between the Cordova plugin and the Node.js engine. + */ + #include "include/node/node_api.h" #include "include/node/uv.h" +#include "cordova-bridge.h" #define NM_F_BUILTIN 0x1 #include #include @@ -9,7 +15,7 @@ #include #include -//Some helper macros from node/test/addons-napi/common.h +// Some helper macros from node/test/addons-napi/common.h // Empty value so that macros here are able to return NULL or void #define NAPI_RETVAL_NOTHING // Intentionally blank #define @@ -34,7 +40,7 @@ if (!(assertion)) { \ napi_throw_error( \ (env), \ - NULL, \ + NULL, \ "assertion (" #assertion ") failed: " message); \ return ret_val; \ } \ @@ -73,7 +79,7 @@ class QueuedFunc { void notify_message(char *s) { napi_env original_env = env; - + napi_handle_scope scope; napi_open_handle_scope(original_env, &scope); @@ -83,7 +89,7 @@ class QueuedFunc { napi_get_reference_value(original_env, original_function_ref, &callback); napi_value global; napi_get_global(original_env, &global); - + napi_value message; napi_create_string_utf8(original_env, s, strlen(s), &message); @@ -161,7 +167,7 @@ napi_value Method_RegisterListener(napi_env env, napi_callback_info info) { } size_t argc = 1; napi_value args[1]; - NAPI_CALL(env, napi_get_cb_info(env,info,&argc,args,NULL,NULL)); + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); NAPI_ASSERT(env, argc == 1, "Wrong number of arguments"); napi_value listener_function = args[0]; @@ -174,7 +180,7 @@ napi_value Method_RegisterListener(napi_env env, napi_callback_info info) { napi_value result; NAPI_CALL(env, napi_create_int32(env, my_little_pool_incrementer, &result)); - + QueuedFunc *af = new QueuedFunc(env, ref_to_function); pool[my_little_pool_incrementer++] = af; @@ -186,10 +192,10 @@ napi_value Method_SendMessage(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value args[1]; - NAPI_CALL(env, napi_get_cb_info(env,info,&argc,args,NULL,NULL)); + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); NAPI_ASSERT(env, argc == 1, "Wrong number of arguments"); - napi_value value_to_log=args[0]; + napi_value value_to_log = args[0]; napi_valuetype valuetype0; NAPI_CALL(env, napi_typeof(env, value_to_log, &valuetype0)); @@ -202,18 +208,18 @@ napi_value Method_SendMessage(napi_env env, napi_callback_info info) { size_t copied; NAPI_CALL(env, napi_get_value_string_utf8(env, value_to_log, NULL, 0, &length)); - //C++ cleans it automatically. - std::unique_ptr unique_buffer(new char[length+1]()); - char *buff=unique_buffer.get(); + // C++ cleans it automatically. + std::unique_ptr unique_buffer(new char[length + 1]()); + char *buff = unique_buffer.get(); - NAPI_CALL(env, napi_get_value_string_utf8(env, value_to_log, buff, length+1, &copied)); + NAPI_CALL(env, napi_get_value_string_utf8(env, value_to_log, buff, length + 1, &copied)); NAPI_ASSERT(env, copied == length, "Couldn't fully copy the message"); - NAPI_ASSERT(env, cordova_callback,"No callback is set in native code to receive the message"); + NAPI_ASSERT(env, cordova_callback, "No callback is set in native code to receive the message"); if (cordova_callback) { cordova_callback(buff); } - + return nullptr; } diff --git a/src/common/cordova-bridge/cordova-bridge.h b/src/common/cordova-bridge/cordova-bridge.h new file mode 100644 index 0000000..870fd5f --- /dev/null +++ b/src/common/cordova-bridge/cordova-bridge.h @@ -0,0 +1,14 @@ +/* + Node.js for Mobile Apps Cordova plugin. + + The bridge APIs between the Cordova plugin and the Node.js engine. + */ + +#ifndef CORDOVA_BRIDGE_H_ +#define CORDOVA_BRIDGE_H_ + +typedef void (*t_bridge_callback)(const char* arg); +void RegisterBridgeCallback(t_bridge_callback); +void SendToNode(const char *message); + +#endif diff --git a/src/ios/CDVNodeJS.hh b/src/ios/CDVNodeJS.hh index 8ec8517..00c5d15 100644 --- a/src/ios/CDVNodeJS.hh +++ b/src/ios/CDVNodeJS.hh @@ -1,3 +1,9 @@ +/* + Node.js for Mobile Apps Cordova plugin. + + The plugin APIs exposed to the Cordova layer. + */ + #import @interface CDVNodeJS : CDVPlugin diff --git a/src/ios/CDVNodeJS.mm b/src/ios/CDVNodeJS.mm index 04c36c8..3f13871 100644 --- a/src/ios/CDVNodeJS.mm +++ b/src/ios/CDVNodeJS.mm @@ -1,8 +1,15 @@ +/* + Node.js for Mobile Apps Cordova plugin. + + Implements the plugin APIs exposed to the Cordova layer and routes messages + between the Cordova layer and the Node.js engine. + */ + #import #import "CDVNodeJS.hh" #import "NodeJSRunner.hh" #import -#import "NativeModule.hpp" +#import "cordova-bridge.h" #ifdef DEBUG #define LOG_FN NSLog(@"%s", __PRETTY_FUNCTION__); @@ -15,7 +22,7 @@ @implementation CDVNodeJS /** - * A method that can be called from the C++ Node NativeModule. + * A method that can be called from the C++ Node native module (i.e. cordova-bridge.ccp). */ void sendMessageToCordova(const char* msg) { CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:[NSString stringWithUTF8String:msg]]; @@ -125,6 +132,13 @@ - (void) startEngine:(CDVInvokedUrlCommand*)command { NSString* scriptPath = nil; CDVPluginResult* pluginResult = nil; NSString* scriptFileName = [command argumentAtIndex:0]; + NSDictionary* options = [command argumentAtIndex:1]; + +#ifdef DEBUG + for (id key in [options allKeys]) { + NSLog(@"Start engine option: %@ -> %@", key, [options objectForKey:key]); + } +#endif if ([scriptFileName length] == 0) { errorMsg = @"Arg was null"; @@ -159,6 +173,13 @@ - (void) startEngineWithScript:(CDVInvokedUrlCommand*)command { NSString* errorMsg = nil; CDVPluginResult* pluginResult = nil; NSString* scriptBody = [command argumentAtIndex:0]; + NSDictionary* options = [command argumentAtIndex:1]; + +#ifdef DEBUG + for (id key in [options allKeys]) { + NSLog(@"Start engine option: %@ -> %@", key, [options objectForKey:key]); + } +#endif if ([scriptBody length] == 0) { errorMsg = @"Script is empty"; diff --git a/src/ios/NativeModule.hpp b/src/ios/NativeModule.hpp deleted file mode 100644 index b69290f..0000000 --- a/src/ios/NativeModule.hpp +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef NativeModule_hpp -#define NativeModule_hpp - -typedef void (*t_bridge_callback)(const char* arg); -void RegisterBridgeCallback(t_bridge_callback); -void SendToNode(const char* msg); - -#endif /* NativeModule_hpp */ diff --git a/src/ios/NodeJSRunner.hh b/src/ios/NodeJSRunner.hh index 9f3f579..031ea16 100644 --- a/src/ios/NodeJSRunner.hh +++ b/src/ios/NodeJSRunner.hh @@ -1,3 +1,8 @@ +/* + Node.js for Mobile Apps Cordova plugin. + + The API to start the Node.js engine from the Cordova plugin native code. + */ @interface NodeJSRunner : NSObject {} diff --git a/src/ios/NodeJSRunner.mm b/src/ios/NodeJSRunner.mm index 6752018..0d8ae45 100644 --- a/src/ios/NodeJSRunner.mm +++ b/src/ios/NodeJSRunner.mm @@ -1,3 +1,10 @@ +/* + Node.js for Mobile Apps Cordova plugin. + + Implements the API to start the Node.js engine from the Cordova plugin + native code. + */ + #import "NodeJSRunner.hh" #import diff --git a/www/nodejs_apis.js b/www/nodejs_apis.js index 9222173..95db3b7 100644 --- a/www/nodejs_apis.js +++ b/www/nodejs_apis.js @@ -11,9 +11,10 @@ Channel.prototype.send = function (msg) { cordova.exec(null, null, 'NodeJS', 'sendMessageToNode', [msg]); }; -const channel = new Channel(); - -function start(filename, callback) { +/** + * Private methods + */ +function startEngine(command, args, callback) { cordova.exec( function(arg) { if (callback) { @@ -26,29 +27,26 @@ function start(filename, callback) { } }, 'NodeJS', - 'startEngine', - [filename] + command, + [].concat(args) ); }; -function startWithScript(script, callback) { - cordova.exec( - function(arg) { - if (callback) { - callback(null); - } - }, - function(err) { - if (callback) { - callback(err); - } - }, - 'NodeJS', - 'startEngineWithScript', - [script] - ); +/** + * Module exports + */ +function start(filename, callback, options) { + options = options || {}; + startEngine('startEngine', [filename, options], callback); }; +function startWithScript(script, callback, options) { + options = options || {}; + startEngine('startEngineWithScript', [script, options], callback); +}; + +const channel = new Channel(); + module.exports = exports = { start, startWithScript,