什么是JavaScript引擎
什么是JavaScript引擎?简单来讲,就是能够提供执行JavaScript代码的运行环境。要解释这一概念,需要了解一些编译原理的基础概念和现代语言需要的一些新编译技术。
首先来看C/C++语言。它们是比较悠久的语言了,实际上就是使用编译器直接将它们编译成本地代码,这一切都是由开发人员在代码编写完成之后实施。 用户只是使用这些编译好的本地代码,这些本地代码被系统的加载器加载执行,这些本地代码(也就是机器指令)由操作系统调度CPU直接执行,无需其它额外的 辅助虚拟机等。这一过程基本上是从源代码开始,然后抽象语法树,之后中间表示,最后到本地代码。
其次,来看看Python等脚本语言。处理脚本语言通常的做法是开发者将写好的代码直接交给用户,用户使用脚本的解释器将脚本文件加载然后解释执 行。当然,现在Python也可以支持将脚本编译生成中间表示,当然这是后话。所以这表示,对于脚本语言,没有开发人员的编译过程,当然也不是绝对,这主 要因为使用场景不一样,它的目的不是高性能。这一过程是源代码,到抽象语法树,再到解释器解释执行。
然后来看看Java语言。其做法可以理解为比较明显的两个阶段,首先是像C++语言一样的编译器,但是,同C++编译器生成的本地代码结果不同,经 过编译器编译之后的是字节码,字节码是平台无关的。在运行字节码阶段,Java的运行环境也就是Java虚拟机会加载字节码,使用解释执行这些字节码。如 果仅是这样,那Java的性能就差C++太多了。现代Java虚拟机一般都引入了JIT技术,也就是前面说的将字节码转变成本地代码来提高执行效率。这主 要两个阶段,第一阶段对时间要求不严格,第二阶段则对每个步骤所花费的时间非常敏感,时间越短越好。
最后回到JavaScript语言上来。前面已经说了它是一种解释性脚本语言。是的,它的确是,但是随着众多工程师不断投入资源来提高它的速度,这 使得它能够使用了Java虚拟机和C++编译器中众多的技术,它的工作方式也在演变。早期也是一样由解释器来解释它们即可,就是将源代码转变成抽象语法 树,然后在抽象语法树上解释执行。随着将Java虚拟机的JIT技术引入,现在的做法是将抽象语法树转成中间表示(也就是字节码),然后通过JIT技术转 成本地代码,这能够大大的提高了执行效率。当然也有些做法直接从抽象语法树生成本地代码的JIT技术,例如V8。这是因为JavaScript跟Java 还是有以下一些区别的:
其一当然是类型,JavaScript是无类型的语言,这使得对于对象的表示和属性的访问上比Java存在比较大的性能损失。不过现在有一些新的技术,参考C++或者Java的类型系统的优点,构建隐式的类型信息,这些后面逐一介绍。
其二是Java语言通常是将源代码编译成字节码,这个同执行阶段是分开的,也就是从源代码到抽象语法树到字节码这段时间的长短是无所谓的(或者说不 是特别重要),所以主要是尽可能的生成高效的字节码即可。而对于JavaScript而言,这些都是在网页和JavaScript文件下载后同执行阶段一 起在网页的加载和渲染过程中来实施的,所以对它们的处理时间也有着很高的要求。
描述JavaScript代码执行的过程,这一过程中因为不同技术的引入,导致非常复杂,而且因为都是在代码运行过程中来处理这些步骤,所以每个阶 段时间越少越好,而且每引入一个阶段都是额外的时间开销,可能最后的本地代码执行效率很高,但是之前步骤如果耗费太多时间的话,最后的执行结果可能并不会 好。所以不同的JavaScript引擎选择了不同的路径,这里先不仔细介绍,后面再描述它们。
所以一个JavaScript引擎不外乎包括以下部分:
第一, 编译器。主要工作是将源代码编译成抽象语法树,然后在某些引擎中还包含将抽象语法树转换成字节码。
第二, 解释器。在某些引擎中,解释器主要是接受字节码,解释执行这个字节码,然后也依赖来及回收机制等。
第三, JIT工具。一个能够能够JIT的工具,将字节码或者抽象语法树转换成本地代码,当然它也需要依赖牢记
第四, 垃圾回收器和分析工具(profiler)。它们负责垃圾回收和收集引擎中的信息,帮助改善引擎的性能和功效。
JavaScript引擎和渲染引擎
前面介绍了网页的工作过程需要使用到两个引擎,也就是渲染引擎和JavaScript引擎。从模块上看,目前,它们是两个独立的模块,分别负责不同 的事情:JavaScript引擎负责执行JavaScript代码,而渲染引擎负责渲染网页。JavaScript引擎提供调用接口被渲染引擎使用,渲 染引擎使用JavaScript引擎来处理JavaScript代码并获取结果。这当然不是全部,事情不是这么简单,JavaScript引擎需要能够访 问渲染引擎构建的DOM树,所以JavaScript引擎通常需要提供桥接的接口,而渲染引擎则根据桥接接口来提供让JavaScript访问DOM的能 力。在现在众多的HTML5能力中,很多都是通过JavaScript接口提供给开发者的,所以这部分同样需要根据桥接接口来实现具体类,以便让 JavaScript引擎能够回调渲染引擎的具体实现。下图是一个简单的关于两者引擎的关系。 在WebKit/Blink项目中,这两种引擎通过桥接接口来访问DOM结构,这对性能来说是一个重大的损失,因为每次JavaScript代码访问 DOM都需要通过复杂和低效的桥接接口来完成。鉴于访问DOM树的普遍性,这是一个常见的问题。希望以后可以减少这一方面的开销。
JavaScriptCore引擎和V8引擎
WebKit中的JavaScript引擎
在WebKit项目中,最初只有JavaScriptCore引擎。在Blink还独立出来之前,为了支持不同的JavaScript引 擎,WebKit设计了一套接口可以切换使用不同的JavaScript引擎(事实上,这一接口会降低性能),所以,WebKit当时可以支持两种类型的 JavaScript引擎,那就是JavaScriptCore引擎和V8引擎。两者都是基于WebKit所提供的接口来同渲染引擎协同工作。
JavaScriptCore引擎
JavaScriptCore引擎是WebKit中默认的引擎,在早期阶段,它的性能不是特别突出。特别是,它只有解释器来解释执行 JavaScript代码。这一效率十分的低效,当然从2008年开始,JavaScriptCore引擎开始一个新的优化工作,就是重新实现了编译器和 字节码解释器,这就是SquirrelFish。该工作对于引擎的性能优化作了比较大的改进。随后,苹果内部代号为”Nitro”的JavaScript 引擎也是基于JavaScriptCore项目的,它的性能还是非常出色的,鉴于其是内部项目,所以具体还有什么特别的处理就不得而知了。在这之后,开发 者们又将内嵌缓存、基于正则表达式的JIT和简单的JIT引入到JavaScriptCore中。然后,又陆续加入的字节码解释器。可以看 出,JavaScriptCore引擎也在不断的高速发展中。
上图是JavaScriptCore最简单的处理部分,它主要是将源代码翻译成抽象语法树,之后是平台无关的字节码,在最初的版本中,字节码会被 JavaScriptCore引擎解释执行。在后面的版本中,逐渐加入了JIT编译器,将热点函数生成本地代码,后面再详细介绍它们。
V8引擎
V8是一个开源项目,也是一个JavaScript引擎的实现。最开始是一帮语言方面的专家设计出来的,之后被Google收购,成为了 JavaScript引擎和众多相关技术的引领者。它的目的很简单,就是为了提高性能,除了性能还是性能。因为之前不管JavaScriptCore引擎 还是其它JavaScript引擎,在当时的情况下,它们的性能都不能令人非常满意。为了达到高性能的JavaScript代码执行效率从而获得更好的网 页浏览效果,它甚至采用直接将JavaScript编译成本地代码的方式。
V8支持众多的操作系统,包括但是不限于Windows、Linux、Android、Mac OS X等。同时它也是能够支持众多的硬件架构,例如IA32、X64、ARM、MIPS等。这么看下来,它将主流软硬件平台一网打尽,由于它是一个开源项目, 开发者可以自由的使用它的强大能力,一个例子就是目前炙手可热的NodeJS项目,它就是基于V8项目的。开源的好处就是大家可以很方便地学习、贡献和使 用,下图是V8引擎最基础的代码执行过程。
从图中可以看出,首先它也是将源代码转变成抽象语法树的,这一点同JavaScriptCore引擎一样,之后两个引擎开始分道扬镳。不同于 JavaScriptCore引擎,V8引擎并不将抽象语法树转变成字节码或者其它中间表示,而是通过JIT编译器的全代码生成器(full code generator)从抽象语法树直接生成本地代码,所以没有像Java一样的虚拟机或者字节码解释器。这样做的原因,主要是因为减少这抽象语法树到字节 码的转换时间,这一切都在网页加载时候完成,虽然可以提高优化的可能,但是这些分析可能带来巨大的时间浪费。当然,缺点也很明显,至少包括两点:第一是某 些JavaScript使用场景其实使用解释器更为合适,因为没有必要生成本地代码;第二是因为没有中间表示,会减少优化的机会因为缺少一个中间表示层。 至于有些文章说的丧失了移植性,个人觉得对于JavaScript这种语言来说不是问题,因为并没有将JavaScript代码先编译然后再运行的明显两 个分开阶段的用法,例如像Java语言那样。但是,针对V8设计思想来说,笔者认为它的理念比较先进,做法虽然比较激进,但是确实给JavaScript 引擎设计者们带来很多新思路。
在之后的版本中,V8工程师们引入了Crankshaft编译器,它能够对热点的JS函数进行生成的分析和优化后再生成本地代码,原因在于不是所有 的JavaScript代码都合适做如此深层次的优化,因为优化本身也需要花费一定的时间。有关V8的细节,希望以后有时间能够介绍它们。
目前V8和JavaScriptCore引擎各有特色,相信两者的发展能够不断推进JavaScript语言的执行性能
1. 概要
JavaScript是一种广泛用于Web客户端开发的脚本语言, 常用来控制浏览器的DOM树,给HTML网页添加动态功能。目前JavaScript遵循的web标准的是ECMAScript262。由于 JavaScript提供了丰富的内置函数、良好的对象机制。所以JavaScript还可以嵌入到某一种宿主语言中,弥补宿主语言的表现力,从而实现快 速、灵活、可定制的开发。
现有的主流浏览器基本上都实现了一个自己的JavaScript引擎。这些JavaScript引擎可以分析、编译和执行JavaScript 脚本。这些JavaScript引擎都是用C或者C++语言写的,都对外提供了API接口。所以在C、C++语言中使用这些JavaScript引擎,嵌 入JavaScript是非常方便的。有一些著名的开源项目都使用了这一种方式,来进行混合的编程,比如Node.js, K-3D等。
已知著名的JavaScript引擎有Google的V8引擎、IE的Trident 引擎、Firefox的SpiderMonkey引擎、Webkit的JavaScriptCore引擎、Opera的Carakan引擎(非开源的,本 文没有分析)等。这些JavaScript引擎对外提供的API接口在细节上各不相同,但是这些API的一个基本的设计思路都类似。C、C++要使用这些 引擎,首先要获得一个全局的Global对象。这个全局的Global对象有属性、方法、事件。比如在JavaScript环境中有一个window窗口 对象。它描述的是一个浏览器窗口。一般JavaScript要引用它的属性和方法时,不需要用“window.xxx”这种形式,而直接使用“xxx”。 它是JavaScript中最大的对象,所有的其他JavaScript对象、函数或者是它的子对象,或者是子对象的子对象。C、C++通过对这个最大的 Global对象调用get、set操作就可以实现与JavaScript进行双向交互了。
下面的介绍涉及到比较多的代码细节,先给个结论吧,不想看C++细节代码可以不看了。
编写语言 | API接口 | C、C++与JavaScript交互(变量、函数、类) | windows xp
vc2005编译 静态库的大小 |
示例EXE的大小 | 执行、解析JavaScript的速度 | |
Google V8 | C++ | C++ | 可以 | 23.1M | 1.1M | 最快 |
Firefox3.5以前 SpiderMonkey | C | C | 可以 | 1.3M | 500K | 慢 |
Firefox高版本SpiderMonkey | C++ | C | 可以 | 15.3M | 1.7M | 一般 |
Webkit JavaScriptCore | C++ | C | 可以 | 26.2M | 1.4M | 一般 |
IE | 未知 | COM | 可以 | 未知 | 100K(没有链接库) | 一般 |
如果优先考虑库的体积,建议使用Firefox的老版本。对执行效率有要求的话,建议使用V8。
2. Google V8
2.1. 介绍
Google Chrome是google 2008年9月发布的浏览器,Chrome的网页渲染部分使用的是Webkit的渲染引擎,Chrome的JavaScript引擎就是大名鼎鼎的V8 了。V8是C++语言编写的,是开放源码的,是所有的JavaScript引擎中速度最块的。其开源项目地址为:http://code.google.com/p/v8。
V8对外的API接口是C++的接口。V8的API定义了几个基本概念:句柄(handle),作用域(scope),上下文环境(Context)。模板(Templates),了解这些基本的概念才可以使用V8。
l 上 下文环境Context就是脚本的运行环境,JavaScript的变量、函数等都存在于上下文环境Context中。Context可以嵌套,即当前函 数有一个Context,调用其它函数时如果又有一个Context,则在被调用的函数中javascript是以最近的Context为准的,当退出这 个函数时,又恢复到了原来的Context。
l 句柄(handle)就是一个指向V8对象的指针,有点像C++的智能指针。所有的v8对象必须使用句柄来操作。没有句柄指向的V8对象,很快会被垃圾回收器回收了。
l 作用域(scope)是句柄的容器,一个作用域(scope)可以有很多句柄(handle)。当离开一个作用域(scope)时,所有在作用域(scope)里的句柄(handle)都会被释放了。
l 模板(Templates)分为函数模板和对象模板,是V8对JavaScript的函数和对象的封装。方便C++语言操作JavaScript的函数和对象。
l V8 API定义了一组类或者模板,用来与JavaScript的语言概念一一对应。比如:
V8的 Function模板与JavaScript的函数对应
V8的Object类与JavaScript的对象对应
V8的String类与JavaScript的字符对应
V8的Script类与JavaScript的脚本文本对应,它可以编译并执行一段脚本。
2.2. C++调用JavaScript
使用V8,在C++中访问Javascript脚本中的内容,首先要调用Context::GetCurrent()->Global()获取到Global全局对象,再通过Global全局对象的Get函数来提取Javascript的全局变量、全局函数、全局复杂对象。C++代码示例如下:
//获取Global对象
Handle<Object>globalObj = Context::GetCurrent()->Global();
//获取Javascrip全局变量
Handle<Value>value = globalObj->Get(String::New(“JavaScript变量名”));
intn = value ->ToInt32()->Value();
//获取Javascrip全局函数,并调用全局函数
Handle<Value>value = globalObj->Get(String::New(“JavaScript函数名”));
Handle<Function> func = Handle<Function>::Cast(value) ;//转换为函数
Local<Value> v1 = Int32::New(0);
Local<Value> v2 = Int32::New(1);
Handle<Value> args[2] = { v1, v2 }; //函数参数
func->Call(globalObj, 2, args);
//获取Javascrip全局对象,并调用对象的函数
Handle<Value>value = globalObj->Get(String::New(“JavaScript复杂对象名”));
Handle<Object> obj = Handle<Object>::Cast(value);//转换为复杂对象
Handle<Value> objFunc = obj ->Get(String::New(“JavaScript对象函数名”));
Handle<Value> args[] = {String::New(“callobject function “)};//函数参数
objFunc->Call(globalObj, 1, args);
2.3. JavaScript调用C++
使用V8,在Javascript脚本中想要访问C++中的内容,必须先将C++的变 量、函数、类注入到Javacsript中。注入时,首先要调用Context::GetCurrent()->Global()获取到 Global对象,再通过Global对象的Set函数来注入全局变量、全局函数、类对象。
全局变量、全局函数的注入过程与上一节的代码类似,这里就省略不写了。比较麻烦的是将C++的类、类对象注入到Javascript中。其基本的过程如下:
1. 首先定义一个C++类,定义一个类对象指针
2. 定义一组C++全局函数,封装V8对C++类的调用,提供给V8进行CALLBACK回调。
3. 最后调用V8 API,将定义的C++类和C++函数注入到Javascript中
在V8的API接口中,还提供了一个“内部数据”(Internal Field)的概念,“内部数据”就是允许V8对象保存一个C++的void*指针。当V8回调C++全局函数时,C++可以设置或者获取该void*指针。
下面是一个将C++类、类变量注入到Javascript中的C++代码示例。
//一个C++类test、
class test
{
public:
test(){number=0;};
voidfunc(){number++;}
int number;
};
//一个全局对象g_test
//目的是:在Javascript中可以直接使用这个对象,例如g_test.func()
test g_test;
//封装V8调用test类构造函数
//在Javascript中如果执行:var t = new test;V8就会调用这个C++函数
//在C++中执行NewInstance函数注入对象时,也会调用这个函数
//默认的test类的构造函数没有参数,C++注入对象时,提供一个额外的参数
v8::Handle<v8::Value> testConstructor(constv8::Arguments& args)
{
v8::Local<v8::Object>self = args.Holder();
//这里假定有两个“内部数据”(Internal Field)
//第一个“内部数据”保存test对象的指针
//第二个“内部数据”为1 就表示这个对象是由C++注入的
//第二个“内部数据”为0 就表示这个对象是JS中自己建立的
if(args.Length())
{
//默认为0,当C++注入对象时,会填充这个“内部数据”
self->SetInternalField(0,v8::External::New(0));
self->SetInternalField(1,v8::Int32::New(1));
}
else
{
self->SetInternalField(0,v8::External::New(new test));
self->SetInternalField(1,v8::Int32::New(0));
}
return self;
}
//封装V8调用test类func方法
//在Javascript中如果执行:t.func();V8就会调用这个C++函数
v8::Handle<v8::Value> testFunc(constv8::Arguments& args)
{
//获取构造函数testConstructor时,设置的对象指针
v8::Local<v8::Object>self = args.Holder();
v8::Local<v8::External> wrap =v8::Local<v8::External>::Cast(self->GetInternalField(0));
void* ptr =wrap->Value();
//调用类方法
static_cast<test*>(ptr)->func();
returnv8::Undefined();
}
//封装V8调用test类成员变量number
//在Javascript中如果执行:t.number;V8就会调用这个C++函数
v8::Handle<v8::Value>getTestNumber(v8::Local<v8::String> property, const v8::AccessorInfo& info)
{
//获取构造函数testConstructor时,设置的对象指针
v8::Local<v8::Object>self = info.Holder();
v8::Local<v8::External> wrap =v8::Local<v8::External>::Cast(self->GetInternalField(0));
void* ptr =wrap->Value();
//返回类变量
returnv8::Int32::New(static_cast<test*>(ptr)->number);
}
//C++类和全局的函数定义好以后,就可以开始将类、变量注入V8 Javascript中了
//获取global对象
v8::Handle<v8::Object> globalObj =context->Global();
//新建一个函数模板,testConstructor是上面定义的全局函数
v8::Handle<v8::FunctionTemplate> test_templ =v8::FunctionTemplate::New(testConstructor);
//设置类名称
test_templ->SetClassName(v8::String::New(“test”));
//获取Prototype,
v8::Handle<v8::ObjectTemplate> test_proto =test_templ->PrototypeTemplate();
//增加类成员函数,testFunc是上面定义的全局函数
test_proto->Set(“func”,v8::FunctionTemplate::New(testFunc));
//设置两个内部数据,用于构造函数testConstructor时,存放类对象的指针。
v8::Handle<v8::ObjectTemplate> test_inst =test_templ->InstanceTemplate();
test_inst->SetInternalFieldCount(2)
//增加类成员变量
//getTestNumber是上面定义的全局函数
//只提供了成员变量的get操作,最后一个参数是成员变量的set操作,这里省略了
test_inst->SetAccessor(v8::String::New(“number”),getTestNumber, 0);
//将test类的定义注入到Javascript中
v8::Handle<v8::Function> point_ctor =test_templ->GetFunction();
globalObj->Set(v8::String::New(“test”),point_ctor);
//新建一个test对象,并使得g_test绑定新建的对象
v8::Handle<v8::Value> flag = v8::Int32::New(1);
v8::Handle<v8::Object> obj =point_ctor->NewInstance(1, &flag);
obj->SetInternalField(0, v8::External::New(&g_test));
globalObj->Set(v8::String::New(“g_test”), obj);
将C++类和类指针注入到V8 JavaScript后,在JavaScript中就可以这样使用了:
g_test.func();
var n = g_test.number;
var t = new test;
3. Firefox SpiderMonkey
3.1. 介绍
Firefox的JavaScript引擎是 SpiderMonkey,SpiderMonkey包括解析器、编译器和JIT即时编译器。JIT即时编译器是JavaScript引擎的核心部分,它 决定了一个JavaScript引擎的效率和速度。Firefox的JIT即时编译器有很多个版本,最早的JIT名称是TraceMonkey, 现在使用的是JägerMonkey,未来准备开发的是IonMonkey。在一些通用的JavaScript测试标准(比如SunSpider) 中,Firefox的JavaScript引擎表现都不好,比IE、V8的执行速度差。Firefox的JS引擎源码地 址:http://ftp.mozilla.org/pub/mozilla.org/js/
SpiderMonkey对外的API接口是C语言的。与WebkitJavaScriptCore的API比较类似,SpiderMonkeyAPI的主要数据结构有:
l JSRuntime:JSRuntime是内存空间,在执行JS函数或脚本之前,首先要调用JS_NewRunTime来初始化一个JSRuntime,JS_NewRunTime只有一个参数,就是内存的大小,当JS引擎使用的内存超出了指定的大小,垃圾回收器就会启动运行。
l JSContext:JavaScript全局上下文。也就是JavaScript的执行环境。
l jsval:JavaScript中的变量
l JSObject:JavaScript中的对象
l JSFunction:JavaScript中的函数
l JSString:JavaScript中的字符
SpiderMonkey API的主要函数有:
l JS_NewRuntime JS_DestroyRuntime:新建和销毁JSRuntime
l JS_NewContext JS_DestroyContext:新建和销毁JSContext
l JS_NewObject:新建一个对象
l JS_SetGlobalObject JS_GetGlobalObject:设置和获取全局对象
l JS_GetProperty JS_SetProperty:JavaScript对象的属性操作
l JS_CallFunctionName:调用JavaScript函数
l JS_DefineFunctions:定义一组JavaScript函数
l JS_InitClass:定义一个JavaScript类
l JS_ExecuteScript:执行JavaScript脚本
SpiderMonkey API还定义了一些宏,用来在jsval与C++类型之间装换
l JSVAL_TO_OBJECT JSVAL_TO_STRING JSVAL_TO_INT:将jsval装换为C++类型
l OBJECT_TO_JSVAL STRING_TO_JSVAL JSVAL_TO_INT:将C++类型装换为jsval
3.2. C++调用JavaScript
使用SpiderMonkey,在C++中访问Javascript脚本中的内容,首先要调用JS_GetGlobalObject获取到Global全局对象,再调用JS_GetProperty函数来提取Javascript的全局变量、全局函数、全局复杂对象。示例如下:
JSRuntime *rt;
JSContext*cx;
JSObject *glob;
//新建JSRuntime
rt = JS_NewRuntime(64L * 1024L* 1024L);
//新建JavaScript全局上下文
cx = JS_NewContext(rt,gStackChunkSize);
//新建JavaScript全局Global对象
glob = JS_NewObject(cx,&global_class, NULL,NULL);
JS_SetGlobalObject(cx, glob);
//获取JavaScript的全局变量
jsval var;
JSBool success = JS_GetProperty(cx, glob, “JavaScript全局变量名”, &var);
long value = JSVAL_TO_INT(var);
//调用全局函数
jsval args[1]; //函数参数
arg[0] = INT_TO_JSVAL(1);
jsval ret;
success= JS_CallFunctionName(cx,glob, “JavaScript全局函数名”, 1, args, &ret);
//处理返回值
long value = JSVAL_TO_INT(ret);
//获取JS复杂对象
sval object;
success = JS_GetProperty(cx,glob, “JS复杂对象名”, & object);
JSObject* obj = JSVAL_TO_OBJECT(var);
jsval retValue;
//调用对象的方法,这里省略了参数和返回值的处理
success = JS_CallFunctionName(cx,obj, “JS复杂对象的方法名”, 0, 0, &retValue);
3.3. JavaScript调用C++
在Javascript脚本中想要访问C++中的内容,必须先将C++的变量、函数、类注入到Javacsript中。注入时,首先要调用JS_GetGlobalObjec获取到Global全局对象,调用JS_SetProperty函数来注入全局变量,调用JS_DefineFunctions注入全局函数、调用JS_InitClass注入类。
全局变量注入过程与上一节的代码类似,这里就省略不写了。
注入全局函数的示例:
//C++全局函数,功能:将传入的两个参数相加
//cx 上下文,obj 目标对象,argc 参数个数,argv参数,rval返回值
JSBool Add (JSContext *cx, JSObject *obj, uintN argc, jsval*argv, jsval *rval){
long value = JSVAL_TO_INT(argv[0]) + JSVAL_TO_INT(argv[1);
*rval = INT_TO_JSVAL(value);
return JS_TRUE;
}
//需要注入到JS中的函数,可以有多个,最后一个必须是{0}
static JSFunctionSpec functions[] =
{
{” Add “, Add, 0},
{0}
};
//注入C++全局函数函数
JS_DefineFunctions(cx, glob, functions);
函数注入后,在JavaScript中可以直接调用这个函数,例如:
var n = Add(100, 100);
使用SpiderMonkey注入C++类的过程和使用V8注入C++类的过程类似。 SpiderMonkey的API也提供了一个类似于V8的“内部数据”的结构。可以给某个JavaScript对象指定一个void*数据,当 SpiderMonkey CallBack回调C++时,就可以获取该void*数据进行操作了。
下面是一个示例代码:
//C++ 类定义
class test
{
public:
test(){number=0;};
voidfunc(){number++;}
int number;
};
test g_test;//变量定义
//定义一个结构,表示对象的“内部数据”
struct testPrivate
{
test* t; //test指针
bool externObject;//是否是C++注入的对象
};
//test类构造函数
//在Javascript中如果执行:var t = new test;V8就会CallBack调用这个C++函数
//在C++中执行NewInstance函数注入对象时,也会调用这个函数
//默认的test类的构造函数没有参数,C++注入对象时,提供一个额外的参数,这个参数就是C++类的指针
JSBool testConstructor(JSContext *cx, JSObject *obj, uintNargc, jsval *argv, jsval *rval)
{
testPrivate* tp =new testPrivate;
if(argc)
{
tp->t =(test*)JSVAL_TO_PRIVATE(argv[0]);//注入的对象
tp->externObject= true;
}
else
{
tp->t = newtest;//不是注入的对象就新建一个
tp->externObject= false;
}
//设置“内部数据”
if ( !JS_SetPrivate(cx, obj, tp) )
return JS_FALSE;
*rval =OBJECT_TO_JSVAL(obj);
return JS_TRUE;
}
//test类析构造函数,JavaScript垃圾回收时,会CallBack调用这个函数
void testDestructor(JSContext *cx, JSObject *obj)
{
//获取设置“内部数据
testPrivate* tp =(testPrivate*)JS_GetPrivate(cx, obj);
if(tp==NULL)
return;
if(!tp->externObject)//注入的对象,不删除
delete tp->t;
delete tp;
}
//封装test类func方法
//在Javascript中如果执行:t.func();就会CallBack调用这个C++函数
JSBool testFunc(JSContext *cx, JSObject *obj, uintN argc,jsval *argv, jsval *rval)
{
//获取设置“内部数据
testPrivate* tp =(testPrivate*)JS_GetPrivate(cx, obj);
tp->t->func();
return JS_TRUE;
}
//定义一个枚举,每个枚举值表示一个test类的成员变量。
enum
{
test_number,
};
//test类的成员变量,可以有多个
JSPropertySpec testProperties[] =
{
{“number”, test_number, JSPROP_ENUMERATE },
{ 0 }
};
//test类的成员变量的get操作
//在Javascript中如果执行:var n= t.number; 就会调用这个C++函数
JSBool testGetProperty(JSContext *cx, JSObject *obj, jsvalid, jsval *vp)
{
if(JSVAL_IS_INT(id))
{
testPrivate* tp= (testPrivate*)JS_GetPrivate(cx, obj);
switch(JSVAL_TO_INT(id))
{
casetest_number:
*vp =INT_TO_JSVAL(tp->t->number);
break;
}
}
return JS_TRUE;
}
//test类的成员变量的set操作
//在Javascript中如果执行:t.number= 100; 就会调用这个C++函数
JSBool testSetProperty(JSContext *cx, JSObject *obj, jsvalid, jsval *vp)
{
if(JSVAL_IS_INT(id))
{
testPrivate* tp= (testPrivate*)JS_GetPrivate(cx, obj);
switch(JSVAL_TO_INT(id))
{
casetest_number:
tp->t->number= JSVAL_TO_INT(*vp);
break;
}
}
return JS_TRUE;
}
//test类方法列表,可以有多个
static JSFunctionSpec testMethods[] = {
{“func”,testFunc, 0},
{0}
};
JSClass testClass =
{
“test”,JSCLASS_HAS_PRIVATE,
JS_PropertyStub,JS_PropertyStub,
testGetProperty,testSetProperty,
JS_EnumerateStub,JS_ResolveStub,
JS_ConvertStub,testDestructor
};
//将test类定义注入JavaScript中
JSObject *newTestObj = JS_InitClass(cx, glob, NULL,&testClass,
testConstructor,0,
NULL,testMethods,
NULL, NULL);
JS_DefineProperties(cx, newTestObj, testProperties);
//在JavaScript脚本中注入一个新对象,并使这个新对象与g_test绑定
jsval arg = PRIVATE_TO_JSVAL(&g_test);
JSObject* gtestObj = JS_ConstructObjectWithArguments(cx,&testClass, NULL, NULL, 1, &arg);
jsval vp = OBJECT_TO_JSVAL(gtestObj);
JS_SetProperty(cx, glob, “g_test”, &vp);
将C++类和类指针注入到V8 JavaScript后,在JavaScript中就可以这样使用了:
g_test.func();
var n = g_test.number;
var t = new test;
4. Webkit JavaScriptCore
4.1. 介绍
WebKit是一个开源的浏览器引 擎。很多浏览器都使用了WebKit浏览器引擎,比如苹果的Safari、Google的Chrome(只使用了排版和渲染部分)。WebKit包含一个 网页排版渲染引擎WebCore和一个脚本引擎JavaScriptCore。JavaScriptCore引擎的API接口是C语言的API接口。关于 JavaScriptCoreAPI的文档资料比较少,不过可以参考苹果公司的JSCocoa文档(基于Objective-C语言的)。苹果的 Safari浏览器重写了JavaScriptCore,新的项目名称是SquirrelFish Extreme。SquirrelFish Extreme的对外API与JavaScriptCore是一样的。Webkit源码SVN地址:http://svn.webkit.org/repository/webkit/trunk
JavaScriptCore API的主要数据结构有:
l JSGlobalContextRef:JavaScript全局上下文。也就是JavaScript的执行环境。
l JSValueRef:JavaScript的一个值,可以是变量、object、函数。
l JSObjectRef:JavaScript的一个object或函数。
l JSStringRef:JavaScript的一个字符串。
l JSClassRef:JavaScript的类。
l JSClassDefinition:JavaScript的类定义,使用这个结构,C、C++可以定义和注入JavaScript的类。
JavaScriptCore API的主要函数有:
l JSGlobalContextCreateJSGlobalContextRelease:创建和销毁JavaScript全局上下文。
l JSContextGetGlobalObject:获取JavaScript的Global对象。
l JSObjectSetPropertyJSObjectGetProperty:JavaScript对象的属性操作。
l JSEvaluateScript:执行一段JS脚本。
l JSClassCreate:创建一个JavaScript类。
l JSObjectMake:创建一个JavaScript对象。
l JSObjectCallAsFunction:调用一个JavaScript函数。
l JSStringCreateWithUTF8CstringJSStringRelease:创建、销毁一个JavaScript字符串
l JSValueToBooleanJSValueToNumber JSValueToStringCopy:JSValueRef转为C++类型
l JSValueMakeBooleanJSValueMakeNumber JSValueMakeString:C++类型转为JSValueRef
4.2. C++调用JavaScript
使用JavaScriptCore,在C++中访问Javascript脚本中的内容,首先要调用JSContextGetGlobalObject获取到Global全局对象,再调用JSObjectGetProperty函数来提取Javascript的全局变量、全局函数、全局复杂对象。示例如下:
//获取Global对象
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
JSObjectRef globalObj = JSContextGetGlobalObject(ctx);
//获取全局变量
JSStringRef varName = JSStringCreateWithUTF8CString(“JavaScript变量名”);
JSValueRef var = JSObjectGetProperty(ctx, globalObj, varName,NULL); JSStringRelease(varName);
//转化为C++类型
int n = JSValueToNumber(ctx, var, NULL);
//获取全局函数
JSStringRef funcName = JSStringCreateWithUTF8CString(“JavaScript函数名”);
JSValueRef func = JSObjectGetProperty(ctx, globalObj, funcName,NULL); JSStringRelease(funcName);
//装换为函数对象
JSObjectRef funcObject = JSValueToObject(ctx,func, NULL);
//组织参数,将两个数值1和2作为两个参数
JSValueRef args[2];
args[0] = JSValueMakeNumber(ctx, 1);
args[1] = JSValueMakeNumber(ctx, 2);
//调用函数
JSValueRef returnValue = JSObjectCallAsFunction(ctx, funcObject,NULL, 2, args, NULL);
//处理返回值
int ret = JSValueToNumber(ctx, returnValue, NULL);
//获取复杂的对象
JSStringRef objName=JSStringCreateWithUTF8CString(“JavaScript复杂对象名”);
JSValueRef obj = JSObjectGetProperty(ctx, globalObj, objName,NULL); JSStringRelease(objName);
//装换为对象
JSObjectRef object = JSValueToObject(ctx,obj, NULL);
//获取对象的方法
JSStringRef funcObjName =JSStringCreateWithUTF8CString(“JavaScript复杂对象的方法”);
JSValueRef objFunc = JSObjectGetProperty(ctx, object, funcObjName,NULL); JSStringRelease(funcObjName);
//调用复杂对象的方法,这里省略了参数和返回值
JSObjectCallAsFunction(ctx, objFunc, NULL, 0, 0, NULL);
4.3. JavaScript调用C++
在Javascript脚本中想要访问C++中的内容,必须先将C++的变量、函数、类注入到Javacsript中。注入时,首先要调用JSContextGetGlobalObject获取到Global全局对象,再调用JSObjectSetProperty函数来注入全局变量、全局函数、类对象。
全局变量、全局函数的注入过程与上一节的代码类似,这里就省略不写了。
将C++的类、类对象注入到Javascript中。其基本的过程如下:
1. 首先定义一个C++类,
2. 定义一组C++全局函数,封装JavaScriptCore对C++类的调用,提供给JavaScriptCore进行CALLBACK回调。
3. 最后调用JSClassCreate函数,将定义的C++类和C++函数注入到Javascript中
C++示例代码如下
//C++ 类定义
class test
{
public:
test(){number=0;};
voidfunc(){number++;}
int number;
};
test g_test;//变量定义
//全局函数,封装test类的func方法调用
JSValueRef testFunc(JSContextRef ctx, JSObjectRef ,JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[],JSValueRef*)
{
test* t =static_cast<test*>(JSObjectGetPrivate(thisObject));
t->func();
returnJSValueMakeUndefined(ctx);
}
//全局函数,封装test类的成员变量number的get操作
JSValueRef getTestNumber(JSContextRef ctx, JSObjectRefthisObject, JSStringRef, JSValueRef*)
{
test* t =static_cast<test*>(JSObjectGetPrivate(thisObject));
returnJSValueMakeNumber(ctx, t->number);
}
//使用一个函数, 创建JavaScript类
JSClassRef createTestClass()
{
//类成员变量定义,可以有多个,最后一个必须是{ 0, 0, 0 }
//也可以指定set操作
static JSStaticValuetestValues[] = {
{“number”, getTestNumber, 0, kJSPropertyAttributeNone },
{ 0, 0, 0, 0}
};
//类的方法定义,可以有多个,最后一个必须是{ 0, 0, 0 }
staticJSStaticFunction testFunctions[] = {
{“func”, testFunc, kJSPropertyAttributeNone },
{ 0, 0, 0 }
};
//定义一个类
staticJSClassDefinition classDefinition = {
0,kJSClassAttributeNone, “test”, 0, testValues, testFunctions,
0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0
};
// JSClassCreate执行后,就创建一个了JavaScript test类
staticJSClassRef t = JSClassCreate(&classDefinition);
return t;
}
//创建JavaScript类
createTestClass ();
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
JSObjectRef globalObj = JSContextGetGlobalObject(ctx);
//新建一个JavaScript类对象,并使之绑定g_test变量
JSObjectRef classObj= JSObjectMake(ctx,testClass(), &g_test);
//将新建的对象注入JavaScript中
JSStringRef objName= JSStringCreateWithUTF8CString(“g_test”);
JSObjectSetProperty(ctx,globalObj,objName,classObj,kJSPropertyAttributeNone,NULL);
将C++类和类指针注入到JavaScript后,在JavaScript中就可以这样使用了:
g_test.func();
var n = g_test.number;
var t = new test;
5. 微软JavaScript
5.1. 介绍
IE的Trident引擎是非开源的,微软JavaScript引擎也是非开源的。微软对外提供了一组COM接口。使用这组COM接口,能够将微软的JavaScript、VBScript嵌入到C、C++宿主语言中。
这组COM接口主要有如下一些定义:
l IDispatch
IDispatch是COM跨语言调用的基本接口。
l 命名空间
这里的“命名空间”的意思相当于JavaScript中的Global对象,宿主语言必须要实现一个“命名空间”。当JavaScript执行脚本,遇见未知的变量、函数、类时候,就会调用该“命名空间”的IDispatch接口,来进行解析和执行。
l IActiveScript
IActiveScript是由JavaScript脚本引擎所实现的接口,C、C++宿主语言调用该COM接口,就可以建立和初始化一个脚本的执行环境。
l IActiveScriptParse
IActiveScriptParse是由JavaScript脚本引擎所实现的接口,该接口可以分析、执行一段JavaScript脚本,并且还可以控制JavaScript脚本的执行过程。
l IActiveScriptSite
IActiveScriptSite是由C、C++宿主语言所实现的接口,该接口可以给JavaScript脚本提供“命名空间”,并且当JavaScript引擎出现错误、异常时,就会使用该接口进行回调通知。
在C++中使用这组COM接口的示例代码如下:
//获取JavaScript的CLSID
CLSID clsid;
CLSIDFromProgID(_T(“JScript”), &clsid);
//创建JavaScript对象,获取IActiveScript接口指针
CComPtr<IActiveScript> activeScript = NULL;
CoCreateInstance(clsid, NULL, CLSCTX_ALL, IID_IActiveScript,(void **)&activeScript);
//创建宿主对象,CMyScriptSite实现了IActiveScriptSite接口
IActiveScriptSite* site = new CComObject<CMyScriptSite>;
site->AddRef();
//将宿主对象IActiveScriptSite与IActiveScript绑定
hr = activeScript->SetScriptSite(site);
//增加一个命名空间,会触发IActiveScriptSite的GetItemInfo方法被调用
_bstr_t name = “test”
hr=activeScript->AddNamedItem(name,SCRIPTITEM_ISVISIBLE|SCRIPTITEM_GLOBALMEMBERS);
//获取脚本解析和执行接口
CComQIPtr<IActiveScriptParse> activeScriptParse= activeScript;
//初始化一个执行环境
activeScriptParse->InitNew();
//解析一段JS
_bstr_t srcipt = “g_test.func()”;
activeScriptParse->ParseScriptText(srcipt,name,NULL,NULL,0,0,0,NULL,NULL);
//开始执行JS
activeScript->SetScriptState(SCRIPTSTATE_STARTED);
//宿主对象CMyScriptSite,必须要实现的一个方法
//与IActiveScript的AddNamedItem对应
//返回给JavaScript一个命名空间
HRESULT CMyScriptSite::GetItemInfo(LPCOLESTR pstrName, DWORDdwReturnMask, IUnknown **ppunkItem, ITypeInfo **ppti){
if (name == _bstr_t(pstrName) )
{ //这里简单将宿主对象自己当做一个“命名空间”返回给JavaScript
HRESULT hr =QueryInterface(IID_IUnknown, (void**)ppunkItem);
return hr;
}
return E_FAIL;
}
5.2. JavaScript与C++的交互
在上一节中,宿主对象返回给JavaScript一个“命名空间”,这个“命名空间”必须实现IDispatch接口。同样的,宿主对象通过调用IActiveScript的GetScriptDispatch方 法,也可以获得JavaScript的IDispatch接口指针。C、C++宿主对象和JavaScript脚本对象都互相持有对方的 IDispatch接口。所以C、C++调用JavaScript,实际上就转化为IDispatch接口的调用。JavaScript调用宿主C、 C++,实际上就转化为宿主语言C、C++对IDispatch接口的实现。
IDispatch是微软跨语言调用的一个标准COM接口。对于这个接口的调用、实现,网上到处都有,这里就忽略不写了。
转载请注明:学时网 » JavaScript引擎研究总结与C、C++与互调用