在大概了解了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 (一)