在大概了解了ChakraCore的顶层结构和其对应的JSRT API 之后,让我们再来看看其他的JSRT API和如何使用它们吧。
为了演示JSRT API,在这一篇里,我们会来尝试如下几件事情:
注入一个简单的JS API 简单了解ChakraCore的异常处理 实现一个简单的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中提供了很多函数来操作一个对象的属性,其中最基本的是这三个: JsGetProperty ,JsSetProperty 和JsDeleteProperty ,他们的作用显而易见,就不多说了,但是有意思的地方是通过他们的函数原型能看到,ChakraCore并没有使用字符串来当做属性的名字,而是使用了一个叫做PropertyId的东西。这个乍一看不是很方便,但是它是有原因的。
2.1.1. PropertyId和PropertyRecord 首先,JS里面的属性不一定是一个字符串,它可以是一个数字或者是Chakra内部指定的一个id,使用字符串没办法覆盖到所有的情况。其次,我们知道一个稍稍复杂的网页里面可能就会包含成千上万个JS对象,他们之间用Property互相连接,所以可想而知Property的数量有多么庞大,虽然通过原型我们能省下一些空间,但是如果每个对象都用原始的Property来存储这些连接,势必要浪费非常的内存。以下是我用Chrome在亚马逊上抓取的一个堆的快照,算是一个例子。
为了解决这些问题,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); 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代码交互时,对异常的处理有两个要求:
当Native回调被调用时,产生的异常必须要处理,不能传给Chakra。如果需要告诉Chakra发生了异常,那么可以通过JsCreateError来创建一个异常,并通过JsSetException告诉Chakra。 在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函数:
添加参数检查,如果传入的key为undefined,那么我们就报错。 用CheckThrow检查所有我们需要调用的函数。 由于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 (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有两种方法:
对每个API都在JS里面单独的进行注册 实现一个通用的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() { 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); 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)); } 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); } 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 (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 { 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 (一)