ChakraCore学习笔记(三):使用JSRT API (一)

大概了解了ChakraCore的顶层结构和其对应的JSRT API之后,让我们再来看看其他的JSRT API和如何使用它们吧。

为了演示JSRT API,在这一篇里,我们会来尝试如下几件事情:

  1. 注入一个简单的JS API
  2. 简单了解ChakraCore的异常处理
  3. 实现一个简单的API Framework

选择这几个目标的原因是因为——毕竟不管在源码层面使用什么脚本引擎,大家一开始最关注的还是怎么注入自己的API来完成自己想要做的事情呀。

1. Hello World

首先让我们建立一个最简单的程序作为开始吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
##include "stdafx.h"

##include "ChakraCore.h"


int main()
{
JsRuntimeHandle runtime;
JsContextRef context;
JsValueRef result;

JsCreateRuntime(JsRuntimeAttributeNone, nullptr, &runtime);
JsCreateContext(runtime, &context);
JsSetCurrentContext(context);

JsSetCurrentContext(JS_INVALID_REFERENCE);
JsDisposeRuntime(runtime);

return 0;
}

2. 注入一个简单的JS API

现在让我们先来实现一个非常简单的API吧——从KeyValue存储中获取一个值:storage.get(key, callback)。

2.1. 修改对象属性

注入API的关键其实非常直接——修改JS中的对象的属性(Property)。Chakra在JSRT API中提供了很多函数来操作一个对象的属性,其中最基本的是这三个: JsGetPropertyJsSetPropertyJsDeleteProperty,他们的作用显而易见,就不多说了,但是有意思的地方是通过他们的函数原型能看到,ChakraCore并没有使用字符串来当做属性的名字,而是使用了一个叫做PropertyId的东西。这个乍一看不是很方便,但是它是有原因的。

2.1.1. PropertyId和PropertyRecord

首先,JS里面的属性不一定是一个字符串,它可以是一个数字或者是Chakra内部指定的一个id,使用字符串没办法覆盖到所有的情况。其次,我们知道一个稍稍复杂的网页里面可能就会包含成千上万个JS对象,他们之间用Property互相连接,所以可想而知Property的数量有多么庞大,虽然通过原型我们能省下一些空间,但是如果每个对象都用原始的Property来存储这些连接,势必要浪费非常的内存。以下是我用Chrome在亚马逊上抓取的一个堆的快照,算是一个例子。
chrome-heap-snapshot-on-amazon

为了解决这些问题,Chakra使用了享元模式,将原始的Property转化成PropertyRecord,并将其保存在ThreadContext之中,从而实现同名Property的共享,而Js对象则使用PropertyRecord来建立和其他对象的联系。另外ChakraCore在PropertyRecord之中并没有保存完整的Property信息,而只保存一些摘要,比如是不是数字,字符串的hash和长度等等,从而进一步压缩内存的使用。

1
2
3
4
5
6
7
8
9
10
11
12
class PropertyRecord : public FinalizableObject
{
// ......
private:
Field(PropertyId) pid;
mutable Field(uint) hash;
Field(bool) isNumeric;
Field(bool) isBound;
Field(bool) isSymbol;
Field(DWORD) byteCount;
// ......
};

现在我们看回JSRT API,JsPropertyIdRef其实就是PropertyRecord的地址。

2.1.2. 创建Property

在了解了PropertyId了之后,我们现在就可以快速的实现一个在Js对象上创建Property的函数了,另外这里我们新建了一个类——ChakraUtils,用来存放和Chakra相关的帮助函数。

1
2
3
4
5
6
7
8
9
10
class ChakraUtils
{
public:
static void SetObjectAsProperty(JsValueRef jsObject, const wchar_t *propertyName, JsValueRef propertyValue)
{
JsPropertyIdRef propertyId;
JsGetPropertyIdFromName(propertyName, &propertyId);
JsSetProperty(jsObject, propertyId, propertyValue, false);
}
};

2.2. 准备API

有了函数来创建对象属性之后,让我们来看看API的实现吧。在JSRT API里,我们可以使用JsCreateFunction为一个Native函数创建一个Js函数,只要它满足固定的函数原型。这里我们就实现了一个简单的StorageGet的函数,用来调用storage.get传入的回调函数,并传回"hello world"的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_Ret_maybenull_ JsValueRef CHAKRA_CALLBACK StorageGet(
_In_ JsValueRef callee,
_In_ bool isConstructCall,
_In_ JsValueRef *arguments,
_In_ unsigned short argumentCount,
_In_opt_ void *callbackState)
{
JsValueRef undefined;
JsGetUndefinedValue(&undefined);

JsValueRef stringValue;
JsPointerToString(L"hello world!", _countof(L"hello world"), &stringValue);

JsValueRef callbackArgs[2] = { undefined, stringValue };
JsCallFunction(arguments[2], callbackArgs, 2, nullptr);

return undefined;
}

这段代码非常直观,除了两个地方有点奇怪:回调函数的参数要放一个undefined在第一个值上?回调函数为什么是第三个参数?这两个问题其实都是一个原因:函数的第一个参数是this指针。

2.3. 注入API

好了,现在我们万事具备了,让我们来注入我们第一个API吧!

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
int main()
{
// ......

JsValueRef globalObject;
ChakraUtils::CheckThrow(JsGetGlobalObject(&globalObject));

JsValueRef storageObject;
JsCreateObject(&storageObject);
ChakraUtils::SetObjectAsProperty(globalObject, L"storage", storageObject);

JsValueRef storageGetFunction;
JsCreateFunction(StorageGet, nullptr, &storageGetFunction);
ChakraUtils::SetObjectAsProperty(storageObject, L"get", storageGetFunction);

std::wstring script = L"(()=>{ var v; storage.get('key', function(value) { v = value; }); return v; })()";

JsValueRef result;
unsigned currentSourceContext = 0;
JsRunScript(script.c_str(), currentSourceContext++, L"", &result);

// Output the string we get from the script.
const wchar_t *resultWC;
size_t stringLength;
JsStringToPointer(result, &resultWC, &stringLength);

std::wstring resultW(resultWC);
std::wcout << resultW << std::endl;

// ......
}

3. 异常处理

在使用JSRT API时,我们可以看到基本所有的函数的返回值都是一个JsErrorCode类型的错误码,如果API调用出错,我们可以通过它来猜测大概是什么原因。Chakra的异常处理也是通过它来完成。

Chakra在和Native代码交互时,对异常的处理有两个要求:

  1. 当Native回调被调用时,产生的异常必须要处理,不能传给Chakra。如果需要告诉Chakra发生了异常,那么可以通过JsCreateError来创建一个异常,并通过JsSetException告诉Chakra。
  2. 在Native代码调用Chakra的函数时,如果发生异常(JsErrorInExceptionState),那么Native代码需要调用JsGetAndClearException来处理异常,不然之后调用任何API都会返回JsErrorInExceptionState错误,哪怕仅仅是创建一个最简单的对象。如果我们不处理这个异常,那么这个异常会被继续传递给上一层调用,直到回到最上层的API调用。

现在让我们再来看看我们上面实现的StorageGet函数,就会发现很多问题。首先,所有API调用的返回值我们都没有检查,其次,假设中间我们调用了别的函数出了异常,我们并没有处理,从而导致Chakra出错。现在让我们来重写这个函数解决这些问题吧。

3.1. JsErrorCode检查函数

首先,为了帮助我们更快的发现错误和简化代码,我们在ChakraUtils类里面添加了一个检查函数。如果发现Chakra返回错误,则抛出一个c++的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ChakraErrorException
{
private:
JsErrorCode _errorCode;

public:
ChakraErrorException(JsErrorCode errorCode) : _errorCode(errorCode) {}
JsErrorCode GetErrorCode() const { return _errorCode; }
};

class ChakraUtils
{
public:
// ......
static void CheckThrow(JsErrorCode errorCode)
{
if (JsNoError != errorCode)
{
throw ChakraErrorException(errorCode);
}
}
// ......
};

3.2. 处理Native的异常

接下来,我们来修改一下下之前的StorageGet函数:

  1. 添加参数检查,如果传入的key为undefined,那么我们就报错。
  2. 用CheckThrow检查所有我们需要调用的函数。
  3. 由于StorageGet中所有的异常必须被接管掉,所以我们需要加入一个try块来正确设置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
31
32
33
34
35
36
37
38
39
40
_Ret_maybenull_ JsValueRef CHAKRA_CALLBACK StorageGet(
_In_ JsValueRef callee,
_In_ bool isConstructCall,
_In_ JsValueRef *arguments,
_In_ unsigned short argumentCount,
_In_opt_ void *callbackState)
{
JsValueRef undefined = nullptr;

try
{
ChakraUtils::CheckThrow(JsGetUndefinedValue(&undefined));

if (arguments[1] == undefined)
{
throw ChakraErrorException(JsErrorInvalidArgument);
}

JsValueRef stringValue;
ChakraUtils::CheckThrow(JsPointerToString(L"hello world!", _countof(L"hello world"), &stringValue));

JsValueRef callbackArgs[2] = { undefined, stringValue };
ChakraUtils::CheckThrow(JsCallFunction(arguments[2], callbackArgs, 2, nullptr));
}
catch (ChakraErrorException &e)
{
// If Chakra is in exception state, then let Chakra rethrow its exception again. Otherwise, create new error.
if (e.GetErrorCode() != JsErrorScriptException && e.GetErrorCode() != JsErrorInExceptionState)
{
JsValueRef message;
ChakraUtils::CheckThrow(JsPointerToString(L"storage.get failed.", _countof(L"storage.get failed."), &message));

JsValueRef error;
ChakraUtils::CheckThrow(JsCreateError(message, &error));
ChakraUtils::CheckThrow(JsSetException(error));
}
}

return undefined;
}

这样,我们的storage.get的雏形就写好啦。

4. 实现一个简单的API Framework

在了解了基本的API之后,我们就可以来创建一个简单的API Framework来简化我们API的实现了。一般来说实现一个API Framework有两种方法:

  1. 对每个API都在JS里面单独的进行注册
  2. 实现一个通用的API通信的通道,然后注入一个脚本来注入所有的API,所有的API内部使用这个API的通信通道,并且在初始化完成之后将该通道隐藏

这两种方法实现起来都差不多,第一种比较直接,但是第二种却使用的更加广泛,比如Chrome,所以这里我们来看一下第二种。

4.1. 创建API调用接口

首先我们来定义一下API调用的接口,因为我们需要定义API的名字,处理回调,并且用脚本注入一个API调用入口,所以自然我们的API接口就可以按照如下方法定义了:

1
2
3
4
5
6
7
8
class API
{
public:
virtual ~API() {}
virtual const wchar_t * GetAPIName() const = 0;
virtual const wchar_t * GetPolyFillScript() const = 0;
virtual _Ret_maybenull_ JsValueRef Execute(_In_ JsValueRef *arguments, _In_ unsigned short argumentCount) = 0;
};

4.2. 创建API管理类

有了API的接口,我们接下来需要创建一个管理类,用来管理所有的API,注入API通信的管道和API的实现。这里需要注意在注入完所有的API之后,我们需要将API通信管道删除,以免被除了我们API实现的其他脚本调用。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
class APIManager
{
public:
APIManager() {}
~APIManager()
{
// Release all the APIs
for each (auto &apiCallback in _apiCallbacks)
{
delete apiCallback.second;
}
}

void RegisterCallbackAPI(_In_ API *callback)
{
std::wstring apiName = callback->GetAPIName();

if (_apiCallbacks[apiName] != nullptr)
{
throw std::exception("API already exists.");
}

_apiCallbacks[apiName] = callback;
}

void InjectAllAPIsToCurrentJsContext()
{
JsValueRef globalObject;
JsGetGlobalObject(&globalObject);

// Inject apiUtils object for creating the communication channel.
JsValueRef apiUtilsObject;
JsCreateObject(&apiUtilsObject);
ChakraUtils::SetObjectAsProperty(globalObject, L"apiUtils", apiUtilsObject);

JsValueRef executeAPIFunction;
JsCreateFunction(APIManager::ExecuteAPI, this, &executeAPIFunction);
ChakraUtils::SetObjectAsProperty(apiUtilsObject, L"executeAPI", executeAPIFunction);

for each (auto &apiCallback in _apiCallbacks)
{
JsValueRef result;
const wchar_t *polyFillScript = apiCallback.second->GetPolyFillScript();
ChakraUtils::CheckThrow(JsRunScript(polyFillScript, currentSourceContext++, L"", &result));
}

// Delete apiUtils, so no other scripts can call our internal APIs.
JsValueRef result;
ChakraUtils::CheckThrow(JsRunScript(L"(()=>{ apiUtils = null; })()", currentSourceContext++, L"", &result));
}

static _Ret_maybenull_ JsValueRef CHAKRA_CALLBACK ExecuteAPI(_In_ JsValueRef callee, _In_ bool isConstructCall, _In_ JsValueRef *arguments, _In_ unsigned short argumentCount, _In_opt_ void *callbackState)
{
APIManager* apiManager = reinterpret_cast<APIManager*>(callbackState);

JsValueRef returnValue = nullptr;
try
{
const wchar_t *rawAPIName;
size_t rawAPINameLength;
ChakraUtils::CheckThrow(JsStringToPointer(arguments[1], &rawAPIName, &rawAPINameLength));

std::wstring apiName(rawAPIName);
auto apiCallbackIt = apiManager->_apiCallbacks.find(apiName);
if (apiCallbackIt == apiManager->_apiCallbacks.end())
{
throw ChakraErrorException(JsErrorInvalidArgument);
}

// Get arguments passed to the actual API
JsValueRef apiArgumentList = arguments[2];

JsPropertyIdRef lengthPropertyId;
ChakraUtils::CheckThrow(JsGetPropertyIdFromName(L"length", &lengthPropertyId));

JsValueRef apiArgumentCountJsValue;
ChakraUtils::CheckThrow(JsGetProperty(apiArgumentList, lengthPropertyId, &apiArgumentCountJsValue));

int apiArgumentCount = 0;
ChakraUtils::CheckThrow(JsNumberToInt(apiArgumentCountJsValue, &apiArgumentCount));

std::unique_ptr<JsValueRef[]> apiArguments(new JsValueRef[apiArgumentCount]);
for (int apiArgumentIndex = 0; apiArgumentIndex < apiArgumentCount; ++apiArgumentIndex)
{
JsValueRef indexJsValue;
ChakraUtils::CheckThrow(JsIntToNumber(apiArgumentIndex, &indexJsValue));
ChakraUtils::CheckThrow(JsGetIndexedProperty(apiArgumentList, indexJsValue, &apiArguments[apiArgumentIndex]));
}

returnValue = apiCallbackIt->second->Execute(apiArguments.get(), apiArgumentCount);
}
catch (ChakraErrorException &e)
{
// If Chakra is in exception state, then let Chakra rethrow its exception again. Otherwise, create new error.
if (e.GetErrorCode() != JsErrorScriptException && e.GetErrorCode() != JsErrorInExceptionState)
{
JsValueRef message;
ChakraUtils::CheckThrow(JsPointerToString(L"storage.get failed.", _countof(L"storage.get failed."), &message));

JsValueRef error;
ChakraUtils::CheckThrow(JsCreateError(message, &error));
ChakraUtils::CheckThrow(JsSetException(error));
}
}

return returnValue;
}

private:
std::unordered_map<std::wstring, API*> _apiCallbacks;
};

4.3. 改写API storage.get

现在我们来用新的API接口来重新实现storage.get吧,这里由于APIManager已经注入了apiUtils.executeAPI作为通信管道,所以我们再实现API的时候,可以先将其保存在闭包内,然后将对API的调用转化为对executeAPI的调用。

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 APIStorageGet : public API
{
public:
const wchar_t * GetAPIName() const override { return L"storage.get"; }

const wchar_t * GetPolyFillScript() const override
{
// Store the apiUtils.executeAPI in a closure and call it whenever storage.get gets called.
return L"(()=>{ var executeAPI = apiUtils.executeAPI; try { storage = storage } catch(err) { storage = {}; }; storage.get = function() { executeAPI('storage.get', arguments); }; })()";
}

_Ret_maybenull_ JsValueRef Execute(_In_ JsValueRef *arguments, _In_ unsigned short argumentCount) override
{
JsValueRef undefined = nullptr;
ChakraUtils::CheckThrow(JsGetUndefinedValue(&undefined));

if (arguments[0] == undefined)
{
throw ChakraErrorException(JsErrorInvalidArgument);
}

JsValueRef stringValue;
ChakraUtils::CheckThrow(JsPointerToString(L"hello world!", _countof(L"hello world"), &stringValue));

JsValueRef callbackArgs[2] = { undefined, stringValue };
ChakraUtils::CheckThrow(JsCallFunction(arguments[1], callbackArgs, 2, nullptr));

return undefined;
}
};

4.4. 改写main函数注入API

现在注入API就变得十分简单了,我们只需要注册我们的API到APIManager,然后在创建JsContext之后,调用APIManager注入所有的API就可以了:

1
2
3
4
5
6
7
8
APIManager apiManager;
apiManager.RegisterCallbackAPI(new APIStorageGet());

JsContextRef context;
JsCreateContext(runtime, &context);
JsSetCurrentContext(context);

apiManager.InjectAllAPIsToCurrentJsContext();

4.5. 更方便的API封装?

是不是觉得调用ChakraCore的API不是那么方便呢?想实现一层真正方便的封装?我们可以参照官方发布的给c#使用的封装:ChakraCore C# Hosting,有兴趣的朋友可以看一看。

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