100 行代码模拟 React-Native 中 JS 与 C 通信过程

1,740 阅读2分钟

我们用 React-Native 做混合开发的时候肯定好奇过 JS 侧和 Native 侧是如何通信的,下面来用简单的代码来模拟这个过程。

React Native 用 JavaScriptCore 作为 JS 的解析引擎,但我对它不是很了解,所以本文使用较为熟悉的轻量 JS 引擎 QuickJs 作为替代,关于 QuickJs 的编译与使用可以参考官方站点 (bellard.org/quickjs/),中文站点 (github.com/quickjs-zh/)

JS 传递消息给 C 侧

  • 创建 JS 上下文
  • QuickJs 引擎没有提供类似 NodeJs 或 JavaScriptCore 中的 global 对象,为了尽量保持与 RN 一致,特意添加一个 global 全局对象
  • 添加 Console 等对象来帮助调试
JSContext *JS_GlobalContextCreate()
{
    JSRuntime *rt = JS_NewRuntime();
    JSContext *ctx = JS_NewContext(rt);
    
    JSValue global = JS_GetGlobalObject(ctx);
    JS_SetPropertyStr(ctx, global, "global", global);
    
    js_std_add_helpers(ctx, 0, NULL);

    return ctx;
}
  • 在 JS 全局对象上添加 bridge 方法,JS 可调用该方法来向 C 侧传递信息
    首先看 C 侧函数 nativeFlushQueueImmediate,RN 侧是通过 folly 库来进行反序列化,本文只模拟到将 JS 侧传来的 JSValue 转换为 JSON 字符串
JSValue nativeFlushQueueImmediate(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
    JSValue quene = JS_JSONStringify(ctx, argv[0], JS_UNDEFINED, JS_UNDEFINED);
    char *chQuene = JS_ToCStringLen2(ctx, NULL, quene, 0);
}
  • 然后将它封装为 JS 侧 built-in 函数,并挂到 JS 侧全局对象上
void installNativeHook(JSContext *ctx, JSValueConst global)
{
    const char *funcname = "nativeFlushQueueImmediate";
    JSValue cfn1 = JS_NewCFunction(ctx, nativeFlushQueueImmediate, funcname, 1);
    JS_DefinePropertyValue(
        ctx, global, JS_NewAtom(ctx, funcname),
        JS_DupValue(ctx, cfn1),
        JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE);
}
  • JS 侧调用该方法即可将消息传给 C 侧
let quene = [[1, 2], [2, 2]]
globa.nativeFlushQueueImmediate(quene)

C 侧向 JS 侧发送消息

  • 首先在 JS 侧全局对象上定义好供 C 侧调用的方法, 约定 C 侧传来的参数是调用的 callbackId 与 args
global.invokeCallback = function (args) {
  let callbackId = args[0]
  args = args.slice(1)
  batchBridge.callbacks[callbackId](args)
}
  • 然后定义 C 侧 invokeJSFunction 函数,会从 JS 侧全局对象上取出 invokeCallback 方法,调用传参即可
void invokeJSFunction(JSContext *ctx, int callbackId, const char* arguments) {
    JSValue global = JS_GetGlobalObject(ctx);
    JSValue callFunction = JS_GetPropertyInternal(ctx, global, JS_NewAtom(ctx, "invokeCallback"), JS_UNDEFINED, 0);
    
    JSValue arg = JS_ParseJSON(ctx, arguments, strlen(arguments), NULL);

    JS_Call(ctx, callFunction, JS_NULL, 1, &arg);
}
  • JS 侧完整代码如下:
let batchBridge = {
  callbacks: []
}

function callNative(arg) {
  global.nativeFlushQueueImmediate([arg])
}

function registerCallback(callback) {
  batchBridge.callbacks.push(callback)
  return batchBridge.callbacks.indexOf(callback)
}

global.invokeCallback = function(args) {
  let callbackId = args[0]
  args = args.slice(1)
  batchBridge.callbacks[callbackId](args)
}

Promise.resolve().then(() => {
  let callback = function (args) {
    console.log("args is: ", args)
    console.log('this is call back!!')
  }

  callNative([1, 2, registerCallback(callback)])
})
  • C 侧完整代码如下,本文只是为了简单模拟,在 nativeFlushQueueImmediate 函数的末尾直接执行了 invokeJSFunction 来执行 JS 侧方法
#include "quickjs-libc.h"
#include <stdio.h>
#include <string.h>

JSContext *JS_GlobalContextCreate()
{
    JSRuntime *rt = JS_NewRuntime();
    JSContext *ctx = JS_NewContext(rt);

    return ctx;
}

void js_install_global_object(JSContext *ctx)
{
    JSValue global = JS_GetGlobalObject(ctx);
    // JS_DefineProperty(ctx, global, JS_NewAtom(ctx, "global"), global, JS_UNDEFINED, JS_UNDEFINED, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE | JS_PROP_HAS_VALUE | JS_PROP_HAS_CONFIGURABLE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE);
    JS_SetPropertyStr(ctx, global, "global", global);
}

JSValue evaluateScript(JSContext *ctx, const char *filename)
{
    uint8_t *buf;
    size_t buf_len;
    JSValue obj;
    int eval_flags;

    buf = js_load_file(ctx, &buf_len, filename);
    eval_flags = 0;

    obj = JS_Eval(ctx, (const char *)buf, buf_len, filename, eval_flags);
    js_std_loop(ctx);

    return obj;
}

JSValue nativeFlushQueueImmediate(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
    JSValue quene = JS_JSONStringify(ctx, argv[0], JS_UNDEFINED, JS_UNDEFINED);
    char *chQuene = JS_ToCStringLen2(ctx, NULL, quene, 0);

    printf("\n quene is: %s \n", chQuene);

    invokeJSFunction(ctx, 0, "[0, 2, \"somearg\"]");
}

void installNativeHook(JSContext *ctx, JSValueConst global)
{
    const char *funcname = "nativeFlushQueueImmediate";
    JSValue cfn1 = JS_NewCFunction(ctx, nativeFlushQueueImmediate, funcname, 1);
    JS_DefinePropertyValue(
        ctx, global, JS_NewAtom(ctx, funcname),
        JS_DupValue(ctx, cfn1),
        JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE);
}

void invokeJSFunction(JSContext *ctx, int callbackId, const char *arguments)
{
    JSValue global = JS_GetGlobalObject(ctx);
    JSValue callFunction = JS_GetPropertyInternal(ctx, global, JS_NewAtom(ctx, "invokeCallback"), JS_UNDEFINED, 0);

    JSValue arg = JS_ParseJSON(ctx, arguments, strlen(arguments), NULL);

    JS_Call(ctx, callFunction, JS_NULL, 1, &arg);
}

int main()
{
    JSContext *ctx = JS_GlobalContextCreate();

    js_install_global_object(ctx);
    js_std_add_helpers(ctx, 0, NULL);

    installNativeHook(ctx, JS_GetGlobalObject(ctx));

    const char *filename = "./test.js";
    evaluateScript(ctx, filename);
}