From 7c922f9751e577154cbc95215bb8c83dbe2254b1 Mon Sep 17 00:00:00 2001 From: Jaime Bernardo Date: Fri, 13 Jul 2018 16:04:55 +0100 Subject: [PATCH] plugin: improved events channel Add 'events' to the channel to allow user defined events. Allow user defined objects to be sent through the channel. --- android/src/main/cpp/native-lib.cpp | 15 +- android/src/main/cpp/rn-bridge.cpp | 328 ++++++++++------- android/src/main/cpp/rn-bridge.h | 4 +- .../RNNodeJsMobileModule.java | 10 +- index.js | 100 +++++- .../builtin_modules/rn-bridge/index.js | 143 +++++++- .../builtin_modules/rn-bridge/package.json | 2 +- ios/NodeRunner.hpp | 4 +- ios/NodeRunner.mm | 19 +- ios/RNNodeJsMobile.h | 2 +- ios/RNNodeJsMobile.m | 8 +- ios/rn-bridge.cpp | 330 +++++++++++------- ios/rn-bridge.h | 16 +- 13 files changed, 668 insertions(+), 313 deletions(-) diff --git a/android/src/main/cpp/native-lib.cpp b/android/src/main/cpp/native-lib.cpp index f06be6d..11aa4ec 100644 --- a/android/src/main/cpp/native-lib.cpp +++ b/android/src/main/cpp/native-lib.cpp @@ -13,12 +13,15 @@ JNIEnv* cacheEnvPointer=NULL; extern "C" JNIEXPORT void JNICALL -Java_com_janeasystems_rn_1nodejs_1mobile_RNNodeJsMobileModule_notifyNode( +Java_com_janeasystems_rn_1nodejs_1mobile_RNNodeJsMobileModule_sendMessageToNodeChannel( JNIEnv *env, jobject /* this */, + jstring channelName, jstring msg) { + const char* nativeChannelName = env->GetStringUTFChars(channelName, 0); const char* nativeMessage = env->GetStringUTFChars(msg, 0); - rn_bridge_notify(nativeMessage); + rn_bridge_notify(nativeChannelName, nativeMessage); + env->ReleaseStringUTFChars(channelName,nativeChannelName); env->ReleaseStringUTFChars(msg,nativeMessage); } @@ -50,15 +53,17 @@ Java_com_janeasystems_rn_1nodejs_1mobile_RNNodeJsMobileModule_getCurrentABIName( #define APPNAME "RNBRIDGE" -void rcv_message(char* msg) { +void rcv_message(const char* channel_name, const char* msg) { JNIEnv *env=cacheEnvPointer; if(!env) return; jclass cls2 = env->FindClass("com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule"); // try to find the class if(cls2 != nullptr) { - jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "sendMessageBackToReact", "(Ljava/lang/String;)V"); // find method + jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "sendMessageBackToReact", "(Ljava/lang/String;Ljava/lang/String;)V"); // find method if(m_sendMessage != nullptr) { + jstring java_channel_name=env->NewStringUTF(channel_name); jstring java_msg=env->NewStringUTF(msg); - env->CallStaticVoidMethod(cls2, m_sendMessage,java_msg); // call method + env->CallStaticVoidMethod(cls2, m_sendMessage, java_channel_name, java_msg); // call method + env->DeleteLocalRef(java_channel_name); env->DeleteLocalRef(java_msg); } } diff --git a/android/src/main/cpp/rn-bridge.cpp b/android/src/main/cpp/rn-bridge.cpp index 52410f8..72dc6c5 100644 --- a/android/src/main/cpp/rn-bridge.cpp +++ b/android/src/main/cpp/rn-bridge.cpp @@ -10,7 +10,9 @@ #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 @@ -67,165 +69,238 @@ #define NAPI_CALL_RETURN_VOID(env, the_call) \ NAPI_CALL_BASE(env, the_call, NAPI_RETVAL_NOTHING) +/** + * Forward declarations + */ +void FlushMessageQueue(uv_async_t* handle); +class Channel; + +/** + * Global variables + */ +std::mutex channelsMutex; +std::map channels; + +/** + * Channel class + */ +class Channel { +private: + napi_env env = NULL; + napi_ref function_ref = NULL; + uv_async_t* queue_uv_handle = NULL; + std::mutex uvhandleMutex; + std::mutex queueMutex; + std::queue messageQueue; + std::string name; + bool initialized = false; -class QueuedFunc { public: - QueuedFunc(napi_env& env, napi_ref& function) : env(env), function(function) { + Channel(std::string name) : name(name) {}; + + // Set up the channel's NAPI data. This method can be called + // only once per channel. + void setNapiRefs(napi_env& env, napi_ref& function_ref) { + this->uvhandleMutex.lock(); + if (this->queue_uv_handle == NULL) { + this->env = env; + this->function_ref = function_ref; + + this->queue_uv_handle = (uv_async_t*)malloc(sizeof(uv_async_t)); + uv_async_init(uv_default_loop(), this->queue_uv_handle, FlushMessageQueue); + this->queue_uv_handle->data = (void*)this; + initialized = true; + uv_async_send(this->queue_uv_handle); + } else { + napi_throw_error(env, NULL, "Channel already exists."); + } + this->uvhandleMutex.unlock(); }; - void notify_message(char *s){ - napi_env original_env = env; + // Add a new message to the channel's queue and notify libuv to + // call us back to do the actual message delivery. + void queueMessage(char* msg) { + this->queueMutex.lock(); + this->messageQueue.push(msg); + this->queueMutex.unlock(); - napi_handle_scope scope; - napi_open_handle_scope(original_env, &scope); + if (initialized) { + uv_async_send(this->queue_uv_handle); + } + }; + + // Process one message at the time, to simplify synchronization between + // threads and minimize lock retention. + void flushQueue() { + char* message = NULL; + bool empty = true; + + this->queueMutex.lock(); + if (!(this->messageQueue.empty())) { + message = this->messageQueue.front(); + this->messageQueue.pop(); + empty = this->messageQueue.empty(); + } + this->queueMutex.unlock(); + + if (message != NULL) { + this->invokeNodeListener(message); + free(message); + } + + if (!empty) { + uv_async_send(this->queue_uv_handle); + } + }; - napi_ref original_function_ref = function; + // Calls into Node to execute the registered Node listener. + // This method is always executed on the main libuv loop thread. + void invokeNodeListener(char* msg) { + napi_handle_scope scope; + napi_open_handle_scope(this->env, &scope); - napi_value callback; - napi_get_reference_value(original_env, original_function_ref, &callback); + napi_value node_function; + napi_get_reference_value(this->env, this->function_ref, &node_function); napi_value global; - napi_get_global(original_env, &global); + napi_get_global(this->env, &global); + + napi_value channel_name; + napi_create_string_utf8(this->env, this->name.c_str(), this->name.size(), &channel_name); napi_value message; - napi_create_string_utf8(original_env, s, strlen(s), &message); + napi_create_string_utf8(this->env, msg, strlen(msg), &message); - napi_value* argv = &message; - size_t argc = 1; + size_t argc = 2; + napi_value argv[argc]; + argv[0] = channel_name; + argv[1] = message; 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; + napi_call_function(this->env, global, node_function, argc, argv, &result); + napi_close_handle_scope(this->env, scope); + }; }; -std::map pool; -int32_t my_little_pool_incrementer=1; - rn_bridge_cb embedder_callback=NULL; -std::mutex queueLock; -std::queue messageQueue; -uv_async_t* queue_uv_handle=NULL; - +/** + * Called by the React Native plug-in to register the callback + * that receives the messages sent from Node. + */ void rn_register_bridge_cb(rn_bridge_cb _cb) { embedder_callback=_cb; } -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); +/** + * Return an existing channel or create a new one if it doesn't exist already. + */ +Channel* GetOrCreateChannel(std::string channelName) { + channelsMutex.lock(); + Channel* channel = NULL; + auto it = channels.find(channelName); + if (it != channels.end()) { + channel = it->second; + } else { + channel = new Channel(channelName); + channels[channelName] = channel; } -} + channelsMutex.unlock(); + return channel; +}; -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); +/** + * Flush the specific channel queue + */ +void FlushMessageQueue(uv_async_t* handle) { + Channel* channel = (Channel*)handle->data; + channel->flushQueue(); } -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"); +/** + * Register a channel and its listener + */ +napi_value Method_RegisterChannel(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[argc]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NAPI_ASSERT(env, argc == 2, "Wrong number of arguments."); + + // args[0] is the channel name + napi_value channel_name = args[0]; + napi_valuetype valuetype0; + NAPI_CALL(env, napi_typeof(env, channel_name, &valuetype0)); + NAPI_ASSERT(env, valuetype0 == napi_string, "Expected a string."); - napi_value listener_function=args[0]; + size_t length; + size_t length_copied; + NAPI_CALL(env, napi_get_value_string_utf8(env, channel_name, NULL, 0, &length)); - napi_valuetype valuetype0; - NAPI_CALL(env, napi_typeof(env, listener_function, &valuetype0)); + std::unique_ptr unique_channelname_buf(new char[length + 1]()); + char* channel_name_utf8 = unique_channelname_buf.get(); + NAPI_CALL(env, napi_get_value_string_utf8(env, channel_name, channel_name_utf8, length + 1, &length_copied)); + NAPI_ASSERT(env, length_copied == length, "Couldn't fully copy the channel name."); - NAPI_ASSERT(env, valuetype0==napi_function, "Expected a function"); + // args[1] is the channel listener + napi_value listener_function = args[1]; + napi_valuetype valuetype1; + NAPI_CALL(env, napi_typeof(env, listener_function, &valuetype1)); + NAPI_ASSERT(env, valuetype1 == 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; + Channel* channel = GetOrCreateChannel(channel_name_utf8); + channel->setNapiRefs(env, ref_to_function); + return nullptr; } -// Let's make something appear on native code. +/** + * Send a message to React Native + */ 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)); + size_t argc = 2; + napi_value args[argc]; - NAPI_ASSERT(env, argc == 1, "Wrong number of arguments"); + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NAPI_ASSERT(env, argc == 2, "Wrong number of arguments."); - napi_value value_to_log=args[0]; + // TODO: arguments parsing and string conversion is done several times, + // replace the duplicated code with a function or a macro. + // args[0] is the channel name + napi_value channel_name = 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)); - } + NAPI_CALL(env, napi_typeof(env, channel_name, &valuetype0)); + NAPI_ASSERT(env, valuetype0 == napi_string, "Expected a string."); 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"); + size_t length_copied; + NAPI_CALL(env, napi_get_value_string_utf8(env, channel_name, NULL, 0, &length)); + std::unique_ptr unique_channelname_buf(new char[length + 1]()); + char* channel_name_utf8 = unique_channelname_buf.get(); + NAPI_CALL(env, napi_get_value_string_utf8(env, channel_name, channel_name_utf8, length + 1, &length_copied)); + NAPI_ASSERT(env, length_copied == length, "Couldn't fully copy the channel name."); + + // args[1] is the message string + napi_value message = args[1]; + + napi_valuetype valuetype1; + NAPI_CALL(env, napi_typeof(env, message, &valuetype1)); + if (valuetype1 != napi_string) { + NAPI_CALL(env, napi_coerce_to_string(env, message, &message)); + } - NAPI_ASSERT(env, embedder_callback,"No callback is set in native code to receive the message"); + length = length_copied = 0; + NAPI_CALL(env, napi_get_value_string_utf8(env, message, NULL, 0, &length)); + std::unique_ptr unique_msg_buf(new char[length + 1]()); + char* msg_buf = unique_msg_buf.get(); + NAPI_CALL(env, napi_get_value_string_utf8(env, message, msg_buf, length + 1, &length_copied)); + NAPI_ASSERT(env, length_copied == length, "Couldn't fully copy the message."); - if(embedder_callback) - { - embedder_callback(buff); + NAPI_ASSERT(env, embedder_callback, "No callback is set in native code to receive the message."); + if (embedder_callback) { + embedder_callback(channel_name_utf8, msg_buf); } - return nullptr; } @@ -236,25 +311,22 @@ napi_value Init(napi_env env, napi_value exports) { napi_status status; napi_property_descriptor properties[] = { DECLARE_NAPI_METHOD("sendMessage", Method_SendMessage), - DECLARE_NAPI_METHOD("registerListener", Method_RegisterListener), + DECLARE_NAPI_METHOD("registerChannel", Method_RegisterChannel), }; NAPI_CALL(env, napi_define_properties(env, exports, sizeof(properties) / sizeof(*properties), properties)); return exports; } -void rn_bridge_notify(const char *message) { +/** + * This method is the public API called by the React Native plugin + */ +void rn_bridge_notify(const char* channelName, const char *message) { int messageLength=strlen(message); - char* messageCopy = (char*)calloc(sizeof(char),messageLength+1); - - strncpy(messageCopy,message,messageLength); - - queueLock.lock(); - messageQueue.push(messageCopy); - queueLock.unlock(); + char* messageCopy = (char*)calloc(sizeof(char),messageLength + 1); + strncpy(messageCopy, message, messageLength); - //lock - if(queue_uv_handle!=NULL) - uv_async_send(queue_uv_handle); + Channel* channel = GetOrCreateChannel(std::string(channelName)); + channel->queueMessage(messageCopy); } NAPI_MODULE_X(rn_bridge, Init, NULL, NM_F_BUILTIN) diff --git a/android/src/main/cpp/rn-bridge.h b/android/src/main/cpp/rn-bridge.h index e845632..3424048 100644 --- a/android/src/main/cpp/rn-bridge.h +++ b/android/src/main/cpp/rn-bridge.h @@ -1,8 +1,8 @@ #ifndef SRC_RN_BRIDGE_H_ #define SRC_RN_BRIDGE_H_ -typedef void (*rn_bridge_cb)(char* arg); +typedef void (*rn_bridge_cb)(const char* channelName, const char* message); void rn_register_bridge_cb(rn_bridge_cb); -void rn_bridge_notify(const char *message); +void rn_bridge_notify(const char* channelName, const char *message); #endif diff --git a/android/src/main/java/com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule.java b/android/src/main/java/com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule.java index e639608..4866a9c 100644 --- a/android/src/main/java/com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule.java +++ b/android/src/main/java/com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule.java @@ -170,8 +170,8 @@ public void run() { } @ReactMethod - public void sendMessage(String msg) { - notifyNode(msg); + public void sendMessage(String channel, String msg) { + sendMessageToNodeChannel(channel, msg); } // Sends an event through the App Event Emitter. @@ -183,14 +183,16 @@ private void sendEvent(String eventName, } // Called from JNI when node sends a message through the bridge. - public static void sendMessageBackToReact(String msg) { + public static void sendMessageBackToReact(String channelName, String msg) { if (_instance != null) { final RNNodeJsMobileModule _moduleInstance = _instance; + final String _channelNameToPass = new String(channelName); final String _msgToPass = new String(msg); new Thread(new Runnable() { @Override public void run() { WritableMap params = Arguments.createMap(); + params.putString("channelName", _channelNameToPass); params.putString("message", _msgToPass); _moduleInstance.sendEvent("nodejs-mobile-react-native-message", params); } @@ -202,7 +204,7 @@ public void run() { public native Integer startNodeWithArguments(String[] arguments, String modulesPath, boolean option_redirectOutputToLogcat); - public native void notifyNode(String msg); + public native void sendMessageToNodeChannel(String channelName, String msg); private void waitForInit() { if (!initCompleted) { diff --git a/index.js b/index.js index 71c4ddb..ef45a64 100644 --- a/index.js +++ b/index.js @@ -2,11 +2,82 @@ import { NativeModules, NativeAppEventEmitter } from 'react-native'; var EventEmitter = require('react-native/Libraries/vendor/emitter/EventEmitter'); +const EVENT_CHANNEL = '_EVENTS_'; + +var channels = {}; + +/* + * This class is defined in rn-bridge/index.js as well. + * Any change made here should be ported to rn-bridge/index.js too. + * The MessageCodec class provides two static methods to serialize/deserialize + * the data sent through the events channel. +*/ +class MessageCodec { + // This is a 'private' constructor, should only be used by this class + // static methods. + constructor(_event, ..._payload) { + this.event = _event; + this.payload = JSON.stringify(_payload); + }; + + // Serialize the message payload and the message. + static serialize(event, ...payload) { + const envelope = new MessageCodec(event, ...payload); + // Return the serialized message, that can be sent through a channel. + return JSON.stringify(envelope); + }; + + // Deserialize the message and the message payload. + static deserialize(message) { + var envelope = JSON.parse(message); + if (typeof envelope.payload !== 'undefined') { + envelope.payload = JSON.parse(envelope.payload); + } + return envelope; + }; +}; + +/** + * Channel super class. + */ +class ChannelSuper extends EventEmitter { + constructor(name) { + super(); + this.name = name; + // Renaming the 'emit' method to 'emitLocal' is not strictly needed, but + // it is useful to clarify that 'emitting' on this object has a local + // scope: it emits the event on the Node side only, it doesn't send + // the event to React Native. + this.emitLocal = this.emit; + delete this.emit; + }; +}; + + const { RNNodeJsMobile } = NativeModules; -class MyEmitter extends EventEmitter {} -MyEmitter.prototype.send=function(msg) { - RNNodeJsMobile.sendMessage(msg); +/** + * Events channel class that supports user defined event types with + * optional arguments. Allows to send any serializable + * JavaScript object supported by 'JSON.stringify()'. + * Sending functions is not currently supported. + * Includes the previously available 'send' method for 'message' events. + */ +class EventChannel extends ChannelSuper { + post(event, ...msg) { + RNNodeJsMobile.sendMessage(this.name, MessageCodec.serialize(event, ...msg)); + }; + + // Posts a 'message' event, to be backward compatible with old code. + send(...msg) { + this.post('message', ...msg); + }; + + processData(data) { + // The data contains the serialized message envelope. + var envelope = MessageCodec.deserialize(data); + this.emitLocal(envelope.event, ...(envelope.payload)); + }; }; const start=function(mainFileName, options) { @@ -21,18 +92,33 @@ const startWithScript=function(script, options) { RNNodeJsMobile.startNodeWithScript(script, options); } -const channel = new MyEmitter(); - +/* + * Dispatcher for all channels. This event is called by the plug-in + * native code to deliver events from Node. + * The channelName field is the channel name. + * The message field is the data. + */ NativeAppEventEmitter.addListener("nodejs-mobile-react-native-message", (e) => { - channel.emit("message",e.message); + if (channels[e.channelName]) { + channels[e.channelName].processData(e.message); + } else { + throw new Error('Error: Channel not found:', e.channelName); + } } ); +function registerChannel(channel) { + channels[channel.name] = channel; +}; + +const eventChannel = new EventChannel(EVENT_CHANNEL); +registerChannel(eventChannel); + const export_object = { start: start, startWithScript: startWithScript, - channel: channel + channel: eventChannel }; module.exports = export_object; diff --git a/install/resources/nodejs-modules/builtin_modules/rn-bridge/index.js b/install/resources/nodejs-modules/builtin_modules/rn-bridge/index.js index 3ca33ed..6bfb3e9 100644 --- a/install/resources/nodejs-modules/builtin_modules/rn-bridge/index.js +++ b/install/resources/nodejs-modules/builtin_modules/rn-bridge/index.js @@ -1,18 +1,139 @@ const EventEmitter = require('events'); -var mybridgeaddon = process.binding('rn_bridge'); +const NativeBridge = process.binding('rn_bridge'); -class MyEmitter extends EventEmitter {} -MyEmitter.prototype.send=function(msg) { - mybridgeaddon.sendMessage(msg); +/** + * Built-in events channel to exchange events between the react-native app + * and the Node.js app. It allows to emit user defined event types with + * optional arguments. + */ +const EVENT_CHANNEL = '_EVENTS_'; + +/** + * This class is defined in the plugin's root index.js as well. + * Any change made here should be ported to the root index.js too. + * The MessageCodec class provides two static methods to serialize/deserialize + * the data sent through the events channel. +*/ +class MessageCodec { + // This is a 'private' constructor, should only be used by this class + // static methods. + constructor(_event, ..._payload) { + this.event = _event; + this.payload = JSON.stringify(_payload); + }; + + // Serialize the message payload and the message. + static serialize(event, ...payload) { + const envelope = new MessageCodec(event, ...payload); + // Return the serialized message, that can be sent through a channel. + return JSON.stringify(envelope); + }; + + // Deserialize the message and the message payload. + static deserialize(message) { + var envelope = JSON.parse(message); + if (typeof envelope.payload !== 'undefined') { + envelope.payload = JSON.parse(envelope.payload); + } + return envelope; + }; +}; + +/** + * Channel super class. + */ +class ChannelSuper extends EventEmitter { + constructor(name) { + super(); + this.name = name; + // Renaming the 'emit' method to 'emitLocal' is not strictly needed, but + // it is useful to clarify that 'emitting' on this object has a local + // scope: it emits the event on the react-native side only, it doesn't send + // the event to Node. + this.emitLocal = this.emit; + delete this.emit; + }; + + emitWrapper(type, ...msg) { + const _this = this; + setImmediate( () => { + _this.emitLocal(type, ...msg); + }); + }; +}; + +/** + * Events channel class that supports user defined event types with + * optional arguments. Allows to send any serializable + * JavaScript object supported by 'JSON.stringify()'. + * Sending functions is not currently supported. + * Includes the previously available 'send' method for 'message' events. + */ +class EventChannel extends ChannelSuper { + post(event, ...msg) { + NativeBridge.sendMessage(this.name, MessageCodec.serialize(event, ...msg)); + }; + + // Posts a 'message' event, to be backward compatible with old code. + send(...msg) { + this.post('message', ...msg); + }; + + processData(data) { + // The data contains the serialized message envelope. + var envelope = MessageCodec.deserialize(data); + this.emitWrapper(envelope.event, ...(envelope.payload)); + }; }; -const channel = new MyEmitter(); +/** + * System channel class. + * Emit pause/resume events when the app goes to background/foreground. + */ +class SystemChannel extends ChannelSuper { + constructor(name) { + super(name); + }; + + processData(data) { + // The data is the event. + this.emitWrapper(data); + }; -var myListener = mybridgeaddon.registerListener( function(msg) { - setImmediate( () => { - channel.emit('message', msg); - }); -}); +}; +/** + * Manage the registered channels to emit events/messages received by the + * react-native app or by the react-native plugin itself (i.e. the system channel). + */ +var channels = {}; + +/* + * This method is invoked by the native code when an event/message is received + * from the react-native app. + */ +function bridgeListener(channelName, data) { + if (channels.hasOwnProperty(channelName)) { + channels[channelName].processData(data); + } else { + console.error('ERROR: Channel not found:', channelName); + } +}; -exports.channel = channel; +/* + * The bridge's native code processes each channel's messages in a dedicated + * per-channel queue, therefore each channel needs to be registered within + * the native code. + */ +function registerChannel(channel) { + channels[channel.name] = channel; + NativeBridge.registerChannel(channel.name, bridgeListener); +}; + +const eventChannel = new EventChannel(EVENT_CHANNEL); +registerChannel(eventChannel); + +module.exports = exports = { + app: systemChannel, + channel: eventChannel +}; diff --git a/install/resources/nodejs-modules/builtin_modules/rn-bridge/package.json b/install/resources/nodejs-modules/builtin_modules/rn-bridge/package.json index b7f2ae4..07e0152 100644 --- a/install/resources/nodejs-modules/builtin_modules/rn-bridge/package.json +++ b/install/resources/nodejs-modules/builtin_modules/rn-bridge/package.json @@ -1,6 +1,6 @@ { "name": "rn-bridge", - "version": "0.1.0", + "version": "0.2.0", "description": "NodeJS for Mobile React Native Bridge", "main": "index.js", "scripts": { diff --git a/ios/NodeRunner.hpp b/ios/NodeRunner.hpp index eb53680..cb62ef9 100644 --- a/ios/NodeRunner.hpp +++ b/ios/NodeRunner.hpp @@ -10,8 +10,8 @@ + (NodeRunner*) sharedInstance; - (void) startEngineWithArguments:(NSArray*)arguments:(NSString*)builtinModulesPath; - (void) setCurrentRNNodeJsMobile:(RNNodeJsMobile*)module; -- (void) sendMessageToNode:(NSString*)message; -- (void) sendMessageBackToReact:(NSString*)message; +- (void) sendMessageToNode:(NSString*)channelName:(NSString*)message; +- (void) sendMessageBackToReact:(NSString*)channelName:(NSString*)message; @property(assign, nonatomic, readwrite) bool startedNodeAlready; @end diff --git a/ios/NodeRunner.mm b/ios/NodeRunner.mm index 28de631..7bd0e54 100644 --- a/ios/NodeRunner.mm +++ b/ios/NodeRunner.mm @@ -3,15 +3,11 @@ #include #include "rn-bridge.h" -void notifyNode(const char* msg) -{ - rn_bridge_notify(msg); -} - -void rcv_message(char* msg) { +void rcv_message(const char* channelName, const char* msg) { @autoreleasepool { + NSString* objectiveCChannelName=[NSString stringWithUTF8String:channelName]; NSString* objectiveCMessage=[NSString stringWithUTF8String:msg]; - [[NodeRunner sharedInstance] sendMessageBackToReact:objectiveCMessage]; + [[NodeRunner sharedInstance] sendMessageBackToReact:objectiveCChannelName:objectiveCMessage]; } } @@ -46,16 +42,17 @@ - (void) setCurrentRNNodeJsMobile:(RNNodeJsMobile*)module _currentModuleInstance=module; } --(void) sendMessageToNode:(NSString*)message +-(void) sendMessageToNode:(NSString*)channelName:(NSString*)message { + const char* c_channelName=[channelName UTF8String]; const char* c_message=[message UTF8String]; - notifyNode(c_message); + rn_bridge_notify(c_channelName, c_message); } --(void) sendMessageBackToReact:(NSString*)message +-(void) sendMessageBackToReact:(NSString*)channelName:(NSString*)message { if(_currentModuleInstance!=nil) { - [_currentModuleInstance sendMessageBackToReact:message]; + [_currentModuleInstance sendMessageBackToReact:channelName:message]; } } diff --git a/ios/RNNodeJsMobile.h b/ios/RNNodeJsMobile.h index 7a05bdf..81296b8 100644 --- a/ios/RNNodeJsMobile.h +++ b/ios/RNNodeJsMobile.h @@ -2,6 +2,6 @@ #import @interface RNNodeJsMobile : NSObject - -(void) sendMessageBackToReact:(NSString*)message; + -(void) sendMessageBackToReact:(NSString*)channelName:(NSString*)message; @end \ No newline at end of file diff --git a/ios/RNNodeJsMobile.m b/ios/RNNodeJsMobile.m index 5e03772..01e2737 100644 --- a/ios/RNNodeJsMobile.m +++ b/ios/RNNodeJsMobile.m @@ -41,10 +41,10 @@ - (id)init RCT_EXPORT_MODULE() -RCT_EXPORT_METHOD(sendMessage:(NSString *)script) +RCT_EXPORT_METHOD(sendMessage:(NSString *)channelName:(NSString *)message) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ - [[NodeRunner sharedInstance] sendMessageToNode:script]; + [[NodeRunner sharedInstance] sendMessageToNode:channelName:message]; }); } @@ -138,11 +138,11 @@ -(void)callStartNodeProject:(NSString *)mainFileName } } --(void) sendMessageBackToReact:(NSString*)message +-(void) sendMessageBackToReact:(NSString*)channelName:(NSString*)message { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ [self.bridge.eventDispatcher sendAppEventWithName:@"nodejs-mobile-react-native-message" - body:@{@"message": message} + body:@{@"channelName": channelName, @"message": message} ]; }); } diff --git a/ios/rn-bridge.cpp b/ios/rn-bridge.cpp index bca2e9c..72dc6c5 100644 --- a/ios/rn-bridge.cpp +++ b/ios/rn-bridge.cpp @@ -10,7 +10,9 @@ #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 @@ -67,194 +69,264 @@ #define NAPI_CALL_RETURN_VOID(env, the_call) \ NAPI_CALL_BASE(env, the_call, NAPI_RETVAL_NOTHING) +/** + * Forward declarations + */ +void FlushMessageQueue(uv_async_t* handle); +class Channel; + +/** + * Global variables + */ +std::mutex channelsMutex; +std::map channels; + +/** + * Channel class + */ +class Channel { +private: + napi_env env = NULL; + napi_ref function_ref = NULL; + uv_async_t* queue_uv_handle = NULL; + std::mutex uvhandleMutex; + std::mutex queueMutex; + std::queue messageQueue; + std::string name; + bool initialized = false; -class QueuedFunc { public: - QueuedFunc(napi_env& env, napi_ref& function) : env(env), function(function) { + Channel(std::string name) : name(name) {}; + + // Set up the channel's NAPI data. This method can be called + // only once per channel. + void setNapiRefs(napi_env& env, napi_ref& function_ref) { + this->uvhandleMutex.lock(); + if (this->queue_uv_handle == NULL) { + this->env = env; + this->function_ref = function_ref; + + this->queue_uv_handle = (uv_async_t*)malloc(sizeof(uv_async_t)); + uv_async_init(uv_default_loop(), this->queue_uv_handle, FlushMessageQueue); + this->queue_uv_handle->data = (void*)this; + initialized = true; + uv_async_send(this->queue_uv_handle); + } else { + napi_throw_error(env, NULL, "Channel already exists."); + } + this->uvhandleMutex.unlock(); }; - void notify_message(char *s){ - napi_env original_env = env; + // Add a new message to the channel's queue and notify libuv to + // call us back to do the actual message delivery. + void queueMessage(char* msg) { + this->queueMutex.lock(); + this->messageQueue.push(msg); + this->queueMutex.unlock(); - napi_handle_scope scope; - napi_open_handle_scope(original_env, &scope); + if (initialized) { + uv_async_send(this->queue_uv_handle); + } + }; + + // Process one message at the time, to simplify synchronization between + // threads and minimize lock retention. + void flushQueue() { + char* message = NULL; + bool empty = true; + + this->queueMutex.lock(); + if (!(this->messageQueue.empty())) { + message = this->messageQueue.front(); + this->messageQueue.pop(); + empty = this->messageQueue.empty(); + } + this->queueMutex.unlock(); + + if (message != NULL) { + this->invokeNodeListener(message); + free(message); + } + + if (!empty) { + uv_async_send(this->queue_uv_handle); + } + }; - napi_ref original_function_ref = function; + // Calls into Node to execute the registered Node listener. + // This method is always executed on the main libuv loop thread. + void invokeNodeListener(char* msg) { + napi_handle_scope scope; + napi_open_handle_scope(this->env, &scope); - napi_value callback; - napi_get_reference_value(original_env, original_function_ref, &callback); + napi_value node_function; + napi_get_reference_value(this->env, this->function_ref, &node_function); napi_value global; - napi_get_global(original_env, &global); + napi_get_global(this->env, &global); + + napi_value channel_name; + napi_create_string_utf8(this->env, this->name.c_str(), this->name.size(), &channel_name); napi_value message; - napi_create_string_utf8(original_env, s, strlen(s), &message); + napi_create_string_utf8(this->env, msg, strlen(msg), &message); - napi_value* argv = &message; - size_t argc = 1; + size_t argc = 2; + napi_value argv[argc]; + argv[0] = channel_name; + argv[1] = message; 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; + napi_call_function(this->env, global, node_function, argc, argv, &result); + napi_close_handle_scope(this->env, scope); + }; }; -std::map pool; -int32_t my_little_pool_incrementer=1; - rn_bridge_cb embedder_callback=NULL; -std::mutex queueLock; -std::queue messageQueue; -uv_async_t* queue_uv_handle=NULL; - +/** + * Called by the React Native plug-in to register the callback + * that receives the messages sent from Node. + */ void rn_register_bridge_cb(rn_bridge_cb _cb) { embedder_callback=_cb; } -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); +/** + * Return an existing channel or create a new one if it doesn't exist already. + */ +Channel* GetOrCreateChannel(std::string channelName) { + channelsMutex.lock(); + Channel* channel = NULL; + auto it = channels.find(channelName); + if (it != channels.end()) { + channel = it->second; + } else { + channel = new Channel(channelName); + channels[channelName] = channel; } -} + channelsMutex.unlock(); + return channel; +}; -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); +/** + * Flush the specific channel queue + */ +void FlushMessageQueue(uv_async_t* handle) { + Channel* channel = (Channel*)handle->data; + channel->flushQueue(); } -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"); +/** + * Register a channel and its listener + */ +napi_value Method_RegisterChannel(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[argc]; + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NAPI_ASSERT(env, argc == 2, "Wrong number of arguments."); + + // args[0] is the channel name + napi_value channel_name = args[0]; + napi_valuetype valuetype0; + NAPI_CALL(env, napi_typeof(env, channel_name, &valuetype0)); + NAPI_ASSERT(env, valuetype0 == napi_string, "Expected a string."); - napi_value listener_function=args[0]; + size_t length; + size_t length_copied; + NAPI_CALL(env, napi_get_value_string_utf8(env, channel_name, NULL, 0, &length)); - napi_valuetype valuetype0; - NAPI_CALL(env, napi_typeof(env, listener_function, &valuetype0)); + std::unique_ptr unique_channelname_buf(new char[length + 1]()); + char* channel_name_utf8 = unique_channelname_buf.get(); + NAPI_CALL(env, napi_get_value_string_utf8(env, channel_name, channel_name_utf8, length + 1, &length_copied)); + NAPI_ASSERT(env, length_copied == length, "Couldn't fully copy the channel name."); - NAPI_ASSERT(env, valuetype0==napi_function, "Expected a function"); + // args[1] is the channel listener + napi_value listener_function = args[1]; + napi_valuetype valuetype1; + NAPI_CALL(env, napi_typeof(env, listener_function, &valuetype1)); + NAPI_ASSERT(env, valuetype1 == 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; + Channel* channel = GetOrCreateChannel(channel_name_utf8); + channel->setNapiRefs(env, ref_to_function); + return nullptr; } -// Let's make something appear on native code. +/** + * Send a message to React Native + */ 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)); + size_t argc = 2; + napi_value args[argc]; - NAPI_ASSERT(env, argc == 1, "Wrong number of arguments"); + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NAPI_ASSERT(env, argc == 2, "Wrong number of arguments."); - napi_value value_to_log=args[0]; + // TODO: arguments parsing and string conversion is done several times, + // replace the duplicated code with a function or a macro. + // args[0] is the channel name + napi_value channel_name = 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)); - } + NAPI_CALL(env, napi_typeof(env, channel_name, &valuetype0)); + NAPI_ASSERT(env, valuetype0 == napi_string, "Expected a string."); 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"); + size_t length_copied; + NAPI_CALL(env, napi_get_value_string_utf8(env, channel_name, NULL, 0, &length)); + std::unique_ptr unique_channelname_buf(new char[length + 1]()); + char* channel_name_utf8 = unique_channelname_buf.get(); + NAPI_CALL(env, napi_get_value_string_utf8(env, channel_name, channel_name_utf8, length + 1, &length_copied)); + NAPI_ASSERT(env, length_copied == length, "Couldn't fully copy the channel name."); + + // args[1] is the message string + napi_value message = args[1]; + + napi_valuetype valuetype1; + NAPI_CALL(env, napi_typeof(env, message, &valuetype1)); + if (valuetype1 != napi_string) { + NAPI_CALL(env, napi_coerce_to_string(env, message, &message)); + } - NAPI_ASSERT(env, embedder_callback,"No callback is set in native code to receive the message"); + length = length_copied = 0; + NAPI_CALL(env, napi_get_value_string_utf8(env, message, NULL, 0, &length)); + std::unique_ptr unique_msg_buf(new char[length + 1]()); + char* msg_buf = unique_msg_buf.get(); + NAPI_CALL(env, napi_get_value_string_utf8(env, message, msg_buf, length + 1, &length_copied)); + NAPI_ASSERT(env, length_copied == length, "Couldn't fully copy the message."); - if(embedder_callback) - { - embedder_callback(buff); + NAPI_ASSERT(env, embedder_callback, "No callback is set in native code to receive the message."); + if (embedder_callback) { + embedder_callback(channel_name_utf8, msg_buf); } - 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_value Init(napi_env env, napi_value exports) { napi_status status; napi_property_descriptor properties[] = { DECLARE_NAPI_METHOD("sendMessage", Method_SendMessage), - DECLARE_NAPI_METHOD("registerListener", Method_RegisterListener), + DECLARE_NAPI_METHOD("registerChannel", Method_RegisterChannel), }; NAPI_CALL(env, napi_define_properties(env, exports, sizeof(properties) / sizeof(*properties), properties)); return exports; } -void rn_bridge_notify(const char *message) { +/** + * This method is the public API called by the React Native plugin + */ +void rn_bridge_notify(const char* channelName, const char *message) { int messageLength=strlen(message); - char* messageCopy = (char*)calloc(sizeof(char),messageLength+1); - - strncpy(messageCopy,message,messageLength); - - queueLock.lock(); - messageQueue.push(messageCopy); - queueLock.unlock(); + char* messageCopy = (char*)calloc(sizeof(char),messageLength + 1); + strncpy(messageCopy, message, messageLength); - //lock - if(queue_uv_handle!=NULL) - uv_async_send(queue_uv_handle); + Channel* channel = GetOrCreateChannel(std::string(channelName)); + channel->queueMessage(messageCopy); } NAPI_MODULE_X(rn_bridge, Init, NULL, NM_F_BUILTIN) diff --git a/ios/rn-bridge.h b/ios/rn-bridge.h index 426ec40..3424048 100644 --- a/ios/rn-bridge.h +++ b/ios/rn-bridge.h @@ -1,8 +1,8 @@ -#ifndef SRC_RN_BRIDGE_H_ -#define SRC_RN_BRIDGE_H_ - -typedef void (*rn_bridge_cb)(char* arg); -void rn_register_bridge_cb(rn_bridge_cb); -void rn_bridge_notify(const char *message); - -#endif +#ifndef SRC_RN_BRIDGE_H_ +#define SRC_RN_BRIDGE_H_ + +typedef void (*rn_bridge_cb)(const char* channelName, const char* message); +void rn_register_bridge_cb(rn_bridge_cb); +void rn_bridge_notify(const char* channelName, const char *message); + +#endif