ChakraCore学习笔记(四):使用JSRT API (二)

在了解了如何注入API之后,我们来看一下一些比较复杂的JSRT API的用法吧。

在这一篇里,我们会来尝试做如下几件事情:

  1. 支持Promise,并用它来创建异步API
  2. 创建多个执行上下文,并创建一个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()
{
// Setting up promise callback queue, after script context created
JsCallbackQueue promiseCallbackQueue;
JsSetPromiseContinuationCallback(PromiseContinuationCallback, &promiseCallbackQueue);

// Run any scripts here ...

// After running scripts
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;
}

好的,现在我们把程序跑起来吧!
storage-get-with-promise

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); \ // Create cross site wrapper for objects from different script context.
}

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(&currentContext));
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));

// Create multiple script context and inject unique context id for each of them.
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]));

// Get the context id from the third script context.
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被正确的输出啦。
object-from-another-script-context

同系列文章:
原创文章,转载请标明出处:Soul Orbit
本文链接地址:ChakraCore学习笔记(四):使用JSRT API (二)