Skip to content
On this page

Javascript 和 C++ 绑定如何实现(WIP)

WARNING

WIP: 前置知识没有介绍,部分细节没有补充,示例代码基本没有注释,缺少解释用图

网页中 JS 与 C++ 需要交互,MB 的 API 支持异步通信,为此实现了两者之间的绑定。

相关的 API 是 mbOnJsQuerymbResponseQuery,基本用法参考获取网页内容

前置知识

V8 的一些基本概念,如 isolatescontextsframes

V8 的一些核心 API,如 NewFromUtf8CompileRunFunctionTemplateGetFunctionGlobal 等。

概述

C++ 与 JS 绑定的一般做法是,用 FunctionTemplate 来创建一个 C++ 函数模板,然后使用 GetFunction 将该函数模板实例化为一个 JS 函数,最后操作 Global 对象(如挂载等),让函数可以在 JS 中调用。

MB 的实现更加复杂,具体见下。

实现

mbOnJsQuery执行后,JS 与 C++ 绑定的时机是创建 JS 上下文之后:

cpp
void MbWebView::onDidCreateScriptContext(wkeWebView webView, void* param, wkeWebFrameHandle frameId, void* context, int extensionGroup, int worldId)
{
    // ...

    common::BindJsQuery::bindFun(self->m_id, self->getClosure().m_jsQueryClosure, webView, frameId);

    // ...
}

注入并执行预先的 JS 代码

为了使 JS 可以使用 window.mbQuery 通知 C++,需要在 JS 运行环境中做一些准备工作,具体办法是将一些代码注入 JS 执行环境。

下面是注入代码内容和执行代码的调用:

注入代码内容,点击查看
cpp
static const char* injectScript = "window.__g_callbackMap__ = {};\n"
"window.__g_callbackMapIdGen__ = 0;\n"
"window.__onMbQuery__ = function(id, customMsg, response) {\n"
"    var cb = window.__g_callbackMap__[id];\n"
"    //console.log('__onMbQuery__ cb:' + customMsg);\n"
"    if (cb) {\n"
"        cb(customMsg, response);\n"
"        delete window.__g_callbackMap__[id];\n"
"    }\n"
"}\n"
"window.__setMbQuery__ = function(func) {\n"
"    window.mbQuery = function(customMsg, request, cb) {\n"
"        var id = -1\n"
"        if ('function' == typeof cb) {\n"
"            id = ++window.__g_callbackMapIdGen__;"
"            window.__g_callbackMap__[id] = cb;\n"
"        }\n"
"        func(customMsg, request, id);\n"
"        //console.log('mbQuery cb:' + typeof cb);\n"
"    }\n"
"}\n"
"function __NumberFormat__() {}\n"
"__NumberFormat__.prototype.format = function(num) { return num; }\n"
"window.Intl = {};\n"
"Intl.NumberFormat = __NumberFormat__;"
;
cpp
void BindJsQuery::bindFun(int64_t hostId, QueryFn* queryFn, wkeWebView webView, wkeWebFrameHandle frameId)
{
    // ...
    wkeRunJsByFrame(webView, frameId, injectScript, false);

    // ...
}

使用 wkeRunJsByFrame 将上面的代码注入到 JS 的执行环境中,而wkeRunJsByFrame 实际调用使用 v8 绑定的 executeScriptAndReturnValue 编译并执行 JS

cpp
v8::Local<v8::Value> ScriptController::executeScriptAndReturnValue(
    v8::Local<v8::Context> context,
    const ScriptSourceCode& source,
    AccessControlStatus accessControlStatus)
{
    // ...

    v8::Local<v8::Script> script;
    if (!v8Call(V8ScriptRunner::compileScript(
                    source, isolate(), accessControlStatus, v8CacheOptions),
            script, tryCatch))
        return result;

    if (!v8Call(V8ScriptRunner::runCompiledScript(isolate(), script,
                    frame()->document()),
            result, tryCatch))
        return result;

    // ...
}

最后,绑定层的 runCompiledScript 实际调用 V8 的 API Script::Run 执行 injectScript

C++ 与 JS 进行绑定

传入 mbOnJsQuery 的回调 mbJsQueryCallback 先是经过包装后作为 m_jsQueryClosure 传入 BindJsQuery::bindFun,再放入 BindJsQuery 类型的 m_closure

cpp
void BindJsQuery::bindFun(int64_t hostId, QueryFn* queryFn, wkeWebView webView, wkeWebFrameHandle frameId)
{
    jsExecState es = wkeGetGlobalExecByFrame(webView, frameId);
    wkeRunJsByFrame(webView, frameId, injectScript, false);

    BindJsQuery* jsQueryFunc = new BindJsQuery(hostId, frameId);
    jsValue jsObj = ::jsFunction(es, jsQueryFunc); 

    jsQueryFunc->m_closure = queryFn; 

    jsValue jsArgs[1] = { jsObj };
    ::jsCallGlobal(es, ::jsGetGlobal(es, "__setMbQuery__"), jsArgs, 1);
}

jsFunction 负责绑定的实现,v8::FunctionTemplate 创建一个创建一个函数模板, functionTemplate->GetFunction(context) 从模板生成一个函数对象。

cpp
jsValue WKE_CALL_TYPE jsFunction(jsExecState es, jsData* data)
{

    // ...
    v8::Local<v8::Object> globalObj = context->Global();

    NativeGetterSetterWrap* wrap = NativeGetterSetterWrap::createWrapAndAddToGlobalObjForRelease(isolate, globalObj);
    wrap->set(data);
    v8::Local<v8::Value> dataLocal = v8::External::New(isolate, wrap);

    v8::Local<v8::FunctionTemplate> functionTemplate = v8::FunctionTemplate::New(isolate, jsFunctionConstructCallback, dataLocal); 
    v8::Local<v8::Function> v8Function;
    if (!functionTemplate->GetFunction(context).ToLocal(&v8Function)) 
        DebugBreak();
    jsValue retVal = createJsValueByLocalValue(isolate, context, v8Function);
    return retVal;
}

回到 bindFun 最后的部分,将jsFunction的结果,还有之前注入 JS 执行环境的__setMbQuery__ 等传入 jsCallGlobal,并且在 JS 环境下执行 __setMbQuery__,之前生成的包含 jsFunctionConstructCallback 的函数对象则传进了 __setMbQuery__ 之中。

__setMbQuery__ 执行后,就注册了 window.mbQueryjsFunctionConstructCallbackwindow.mbQuery 的闭包中。后续 JS 使用 window.mbQuery 实现调用 C++。

cpp
void BindJsQuery::bindFun(int64_t hostId, QueryFn* queryFn, wkeWebView webView, wkeWebFrameHandle frameId)
{

    // ...

    jsValue jsObj = ::jsFunction(es, jsQueryFunc);

    jsQueryFunc->m_closure = queryFn;

    jsValue jsArgs[1] = { jsObj };
    ::jsCallGlobal(es, ::jsGetGlobal(es, "__setMbQuery__"), jsArgs, 1); 
}

当页面使用 window.mbQuery 调用 C++ 时, jsFunctionConstructCallback 就会执行,而调用 MB API 传入的 mbJsQueryCallbackwrap->jsDataObj->callAsFunction(execState, wkeData, argv, argsLength) 执行。

createJsValueByLocalValue 则负责处理 JS 传入的参数,其结果 jsValue 类型后续可以调用 mbJsToString 等方法使 C++ 接收 JS 参数。

到这里,这就实现了 JS 调用 C++ 的内容。

cpp
void jsFunctionConstructCallback(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    v8::Isolate* isolate = args.GetIsolate();
    v8::Local<v8::Value> data = args.Data();

    NativeGetterSetterWrap* wrap = static_cast<NativeGetterSetterWrap*>(v8::External::Cast(*data)->Value());

    JsExecStateInfo* execState = JsExecStateInfo::create();
    execState->args = &args;
    execState->isolate = isolate;
    v8::Local<v8::Context> context = execState->isolate->GetCurrentContext();
    execState->context.Reset(isolate, context);
    
    jsValue wkeData = createJsValueByLocalValue(isolate, context, data);

    int argsLength = args.Length();
    jsValue* argv = new jsValue[argsLength];

    for (int i = 0; i < argsLength; ++i)
        argv[i] = createJsValueByLocalValue(isolate, context, args[i]);

    wke::AutoDisableFreeV8TempObejct autoDisableFreeV8TempObejct;
    jsValue retWkeValue = wrap->jsDataObj->callAsFunction(execState, wkeData, argv, argsLength);
    args.GetReturnValue().Set(getV8Value(retWkeValue, context));
}

mbResponseQuery C++ 返回 JS 结果

mbResponseQuery 的实现就简单很多,主要逻辑就是获取之前注入页面的 __onMbQuery__ 并执行,此时 JS 在 window.mbQuery 中传入的回调就会收到返回的结果。

cpp
void MB_CALL_TYPE mbResponseQuery(mbWebView webviewHandle, int64_t queryId, int customMsg, const utf8* response)
{
    std::string* requestString = new std::string(response ? response: "");
    common::ThreadCall::callBlinkThreadAsync(MB_FROM_HERE, [webviewHandle, queryId, customMsg, requestString] {
        std::pair<wkeWebFrameHandle, int>* idInfo = (std::pair<wkeWebFrameHandle, int>*)queryId;
        mb::MbWebView* webview = (mb::MbWebView*)common::LiveIdDetect::get()->getPtr(webviewHandle);

        wkeWebView wkeWebview = webview->getWkeWebView();

        jsExecState es = wkeGetGlobalExecByFrame(wkeWebview, idInfo->first);
        jsValue windowVal = jsGlobalObject(es);
        jsValue jsFunc = jsGet(es, windowVal, "__onMbQuery__");

        jsValue args[3];
        args[0] = jsInt(idInfo->second);
        args[1] = jsInt(customMsg);
        args[2] = jsString(es, requestString->c_str());
        jsCallGlobal(es, jsFunc, args, 3);       
    });
}

现在,JS 与 C++ 的绑定工作就基本结束了。