在了解了如何注入API 之后,我们来看一下一些比较复杂的JSRT API的用法吧。
在这一篇里,我们会来尝试做如下几件事情:
支持Promise,并用它来创建异步API
创建多个执行上下文,并创建一个API用以获取其他执行上下文里面的JS对象
1. 异步调用
Javascript现在在后台都如此被广泛的应用,其最大的好处就在于方便实现异步调用,随着promise和async function的加入,现在在JS中实现异步也越来越方便,代码逻辑也越来越清晰 ,那么现在我们也来跟上时代,把我们的storage.get API改造成promise吧。
1.1. 修改storage.get的polyfill script
首先,我们把我们注入的API修改成promise的形式,内部实现我们依然可以使用回调函数,这样我们C++的实现部分就不需要做任何的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class APIStorageGet : public API{ public : const wchar_t * GetPolyFillScript () const override { return L"(() => {" L" var executeAPI = apiUtils.executeAPI;" L" try { storage = storage } catch(err) { storage = {}; };" L" storage.get = function() { " L" return new Promise(function(resolve) {" L" var apiArguments = [ arguments[0], function (data) { resolve(data); } ];" L" executeAPI('storage.get', apiArguments);" L" });" L" };" L"})()" ; } };
1.2. 添加任务队列用以支持promise
和callback不同,promise都是异步执行的。当一个promise被创建时,ChakraCore会产生一个ContinuationCallback并传递给host,当脚本执行完毕返回到host之后,如果host认为这个时候可以开始继续所有的promise了,我们就可以回调这些callback,让promise继续执行了。所以也很明显,为了支持promise,我们必须要实现一个任务队列。
实现任务队列的时候我们需要对传入的回调函数调用JsAddRef ,这个是因为这个回调函数是被Chakra外部的程序拿着,所以Chakra的垃圾回收器并不知道这个信息,所以如果我们不调用这个函数告诉Chakra这个对象上有一个外部引用,那么这个对象就会在所有内部引用消失后被回收掉,等我们再调用时,这个对象就成了野指针了。所以这个调用非常重要,同样的在执行完这个回调后,我们会将其从任务队列中删除,那么我们就需要调用JsRelease 来告诉Chakra,这个外部引用已经消失了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class JsCallbackQueue { public : bool IsEmpty () const { return _callbacks.empty (); } void Push (JsValueRef callback) { _callbacks.push (callback); JsAddRef (callback, nullptr ); } void ExecuteAll () { JsValueRef global; JsGetGlobalObject (&global); while (!IsEmpty ()) { JsValueRef callback = _callbacks.front (); _callbacks.pop (); JsValueRef result; JsCallFunction (callback, &global, 1 , &result); JsRelease (callback, nullptr ); } } private : std::queue<JsValueRef> _callbacks; };
有了任务队列之后,我们就可以使用JsSetPromiseContinuationCallback来注册回调函数了,需要注意的是:每一个ScriptContext都有一个单独的PromiseContinuationCallback ,所以每创建一个ScriptContext,我们就需要注册一个PromiseContinuationCallback。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void CALLBACK PromiseContinuationCallback (JsValueRef callback, void *callbackState) { JsCallbackQueue *queue = reinterpret_cast <JsCallbackQueue*>(callbackState); queue->Push (callback); } int main () { JsCallbackQueue promiseCallbackQueue; JsSetPromiseContinuationCallback (PromiseContinuationCallback, &promiseCallbackQueue); promiseCallbackQueue.ExecuteAll () }
1.3. 添加console.log API用以异步输出字符串
因为换成Promise之后,函数变成异步执行了,所以我们没有办法用返回值来回传API的返回值了,所以我们这里需要再实现一个简单的console.log来输出一个字符串,用于校验我们程序的运行结果。实现非常简单,这里就不多说了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class APIConsoleLog : public API{ public : const wchar_t * GetAPIName () const override { return L"console.log" ; } const wchar_t * GetPolyFillScript () const override { return L"(() => { var executeAPI = apiUtils.executeAPI; try { console = console; } catch(err) { console = {}; }; console.log = function() { executeAPI('console.log', arguments); }; })()" ; } _Ret_maybenull_ JsValueRef Execute (_In_ JsValueRef *arguments, _In_ unsigned short argumentCount) override { const wchar_t *resultWC; size_t stringLength; ChakraUtils::CheckThrow (JsStringToPointer (arguments[0 ], &resultWC, &stringLength)); std::wstring resultW (resultWC) ; std::wcout << resultW << std::endl; JsValueRef undefined = nullptr ; ChakraUtils::CheckThrow (JsGetUndefinedValue (&undefined)); return undefined; } };
1.4. 在main函数里运行脚本,测试新的API
现在一切准备就绪,我们可以来编写我们的main函数运行测试代码了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int main () { APIManager apiManager; apiManager.RegisterCallbackAPI (new APIConsoleLog ()); apiManager.RegisterCallbackAPI (new APIStorageGet ()); JsRuntimeHandle runtime; ChakraUtils::CheckThrow (JsCreateRuntime (JsRuntimeAttributeNone, nullptr , &runtime)); JsContextRef context; ChakraUtils::CheckThrow (JsCreateContext (runtime, &context)); ChakraUtils::CheckThrow (JsSetCurrentContext (context)); apiManager.InjectAllAPIsToCurrentJsContext (); JsCallbackQueue promiseCallbackQueue; ChakraUtils::CheckThrow (JsSetPromiseContinuationCallback (PromiseContinuationCallback, &promiseCallbackQueue)); std::wstring script = L"(()=>{ storage.get('key').then(function(data) { console.log(data); }); })()" ; JsValueRef result; ChakraUtils::CheckThrow (JsRunScript (script.c_str (), currentSourceContext++, L"" , &result)); promiseCallbackQueue.ExecuteAll (); ChakraUtils::CheckThrow (JsSetCurrentContext (JS_INVALID_REFERENCE)); ChakraUtils::CheckThrow (JsDisposeRuntime (runtime)); return 0 ; }
好的,现在我们把程序跑起来吧!
2. 访问其他执行上下文里的对象
我们知道执行上下文(JsContext)是用来提供隔离的JS执行环境的,不同的执行上下文之间的对象是默认互相看不见的,可是有的时候我们希望能访问对方的对象,这时候应该怎么办呢?
2.1. CrossSite代理对象
其实跨执行上下文传递对象在ChakraCore里面异常的简单,我们并不需要对传递的对象做任何额外的处理。在JSRT API被调用的时候,他们会主动检查所有的参数,如果发现任何的JS对象来自别的执行上下文,ChakraCore会主动将该对象拷贝到当前ScriptContext的环境中或者为其创建一个CrossSite对象当作Proxy(CrossSite代理对象有时候并不是一个真正的对象,但是我们这里可以先简单这么理解,后面看对象模型的时候我们再来看CrossSite代理的实现),而我们代码不需要做任何的调整。现在我们就来看一个例子吧:JsCallFunction。
在JsCallFunction中,我们可以看到一开始这个函数就对所有的参数用VALIDATE_INCOMING_REFERENCE进行了参数检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 CHAKRA_API JsCallFunction (_In_ JsValueRef function, _In_reads_(cargs) JsValueRef *args, _In_ ushort cargs, _Out_opt_ JsValueRef *result) { return ContextAPIWrapper <true >([&](Js::ScriptContext *scriptContext, TTDRecorder& _actionEntryPopper) -> JsErrorCode { for (int index = 0 ; index < cargs; index++) { VALIDATE_INCOMING_REFERENCE (args[index], scriptContext); } }); }
而我们看这个VALIDATE_INCOMING_REFERENCE的定义的时候,我们就会发现其中有一行很可疑:MARSHAL_OBJECT。没错,这个宏就是用来判断ScriptContext和创建CrossSite代理对象的宏了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ##define VALIDATE_INCOMING_REFERENCE(p, scriptContext) \ { \ VALIDATE_JSREF (p); \ if (Js::RecyclableObject::Is (p)) \ { \ MARSHAL_OBJECT (p, scriptContext) \ } \ } ##define MARSHAL_OBJECT(p, scriptContext) \ Js::RecyclableObject* __obj = Js::RecyclableObject::FromVar (p); \ if (__obj->GetScriptContext () != scriptContext) \ { \ if (__obj->GetScriptContext ()->GetThreadContext () != scriptContext->GetThreadContext ()) \ { \ return JsErrorWrongRuntime; \ } \ p = Js::CrossSite::MarshalVar (scriptContext, __obj); \ }
2.2. 举个栗子
为了实验我们刚刚看到的代码,我们来写个小程序创建多个ScriptContext,并实现一个context.get的API用以获取各个ScriptContext中的GlobalObject吧。
首先,我们先定义好一个全局变量,用来保存所有的ScriptContext。
1 2 3 ##define JS_CONTEXT_COUNT 5 std::vector<JsContextRef> s_contexts;
然后,我们来实现context.get的API,代码用了上一章我们实现的APIManager,实现并不复杂。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class APIContextGet : public API{ public : const wchar_t * GetAPIName () const override { return L"context.get" ; } const wchar_t * GetPolyFillScript () const override { return L"(()=>{ var executeAPI = apiUtils.executeAPI; try { context = context; } catch(err) { context = {}; }; context.get = function() { executeAPI('context.get', arguments); }; })()" ; } _Ret_maybenull_ JsValueRef Execute (_In_ JsValueRef *arguments, _In_ unsigned short argumentCount) override { if (argumentCount < 2 ) { throw ChakraErrorException (JsErrorInvalidArgument); } JsValueRef undefined = nullptr ; ChakraUtils::CheckThrow (JsGetUndefinedValue (&undefined)); int contextIndex = 0 ; ChakraUtils::CheckThrow (JsNumberToInt (arguments[0 ], &contextIndex)); if (contextIndex >= static_cast <int >(s_contexts.size ())) { throw ChakraErrorException (JsErrorInvalidArgument); } JsContextRef currentContext; ChakraUtils::CheckThrow (JsGetCurrentContext (¤tContext)); ChakraUtils::CheckThrow (JsSetCurrentContext (s_contexts[contextIndex])); JsValueRef contextGlobalObject; ChakraUtils::CheckThrow (JsGetGlobalObject (&contextGlobalObject)); ChakraUtils::CheckThrow (JsSetCurrentContext (currentContext)); JsValueRef callbackArgs[2 ] = { undefined, contextGlobalObject }; ChakraUtils::CheckThrow (JsCallFunction (arguments[1 ], callbackArgs, 2 , nullptr )); return undefined; } }
最后,我们在main函数里创建多个ScriptContext,并给它们都注入一个contextId,然后我们通过context.getAPI来获取第3个Context中的Id,并输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int main () { APIManager apiManager; apiManager.RegisterCallbackAPI (new APIContextGet ()); apiManager.RegisterCallbackAPI (new APIConsoleLog ()); apiManager.RegisterCallbackAPI (new APIStorageGet ()); JsRuntimeHandle runtime; ChakraUtils::CheckThrow (JsCreateRuntime (JsRuntimeAttributeNone, nullptr , &runtime)); s_contexts.resize (JS_CONTEXT_COUNT, nullptr ); for (int i = 0 ; i < JS_CONTEXT_COUNT; ++i) { ChakraUtils::CheckThrow (JsCreateContext (runtime, &s_contexts[i])); ChakraUtils::CheckThrow (JsSetCurrentContext (s_contexts[i])); apiManager.InjectAllAPIsToCurrentJsContext (); wchar_t scriptBuffer[256 ]; wsprintfW (scriptBuffer, L"(()=>{ contextId = 'context-id-%d'; })()" , i); JsValueRef result; ChakraUtils::CheckThrow (JsRunScript (scriptBuffer, currentSourceContext++, L"" , &result)); } ChakraUtils::CheckThrow (JsSetCurrentContext (s_contexts[0 ])); std::wstring script = L"(()=>{ context.get(3, function(contextGlobalObject) { console.log(contextGlobalObject.contextId); }); })()" ; JsValueRef result; ChakraUtils::CheckThrow (JsRunScript (script.c_str (), currentSourceContext++, L"" , &result)); ChakraUtils::CheckThrow (JsSetCurrentContext (JS_INVALID_REFERENCE)); ChakraUtils::CheckThrow (JsDisposeRuntime (runtime)); return 0 ; }
这样,我们就可以看到contextId被正确的输出啦。
原创文章,转载请标明出处: Soul Orbit 本文链接地址: ChakraCore学习笔记(四):使用JSRT API (二)