您的位置:首页 > Web前端 > JavaScript

[转]基于 C 语言的 JavaScript 引擎探索

2011-04-20 21:00 337 查看
作者:邱 俊涛, 软件工程师, 云电同方研发中心

来源:http://www.ibm.com/developerworks/cn/linux/l-cn-spidermonkey/



基础知识

SpiderMonkey 简介
和其他的 JavaScript 引擎一样,SpiderMonkey 不直接提供像 DOM 这样的对象,而是提供解析,执行 JavaSccript 代码,垃圾回收等机制。SpidlerMonkey 是一个在 Mozilla 之下的开源项目,要使用 SpiderMonkey,需要下载其源码,然后编译为静态 / 动态库使用。
要在自己的应用程序中使用 SpiderMonkey,首先需要了解以下三个核心概念:
运行时环境运行时环境是所有 JavaScript 变量,对象,脚本以及代码的上下文所存在的空间。每一个上下文对象,以及所有的对象均存在于此。一般应用仅需要一个运行时即可。
上下文上下文即脚本执行的环境,在 SpiderMonkey 中,上下文可以编译执行脚本,可以存取对象的属性,调用 JavaScript 的函数,转换类型,创建 / 维护对象等。几乎所有的 SpiderMonkey 函数都需要上下文作为其第一个参数 (JSContext *)。
上下文与线程密不可分,一般来讲,单线程应用可以使用一个上下文来完成所有的操作,每一个上下文每次只能完成一个操作,所有在多线程应用中,同一时刻只能有一个线程来使用上下文对象。一般而言,多线程应用中,每个线程对应一个上下文。
全局对象全局对象包含 JavaScript 代码所用到的所有类,函数,变量。在 DOM 操作中,我们使用的:

alter("something");

事实上使用的是全局变量 window 的一个属性 alter( 这个属性正好是一个函数 ),事实上上边的语句在执行时会别解释为:

window.alter("something");

三者的关系如下图所示:

图 1. 引擎内部结构依赖关系




安装 SpiderMonkey
首先从 SpiderMonkey 的代码库中下载其源码包 js-1.7.0.tar.gz 本文在 Linux 环境下编译,SpiderMonkey 的编译安装很容易:

# 解压缩
 tar xvzf js-1.7.0.tar.gz 

 # 切换至源码目录
 cd js-1.7.0/src 

 # 编译
 make -f Makefile.ref

编译完成之后,会生成一个新的目录,这个目录的名称依赖于平台,比如在 Linux 下,名称为:Linux_All_DBG.OBJ,其中包含静态链接库 libjs.a 和动态链接库 libjs.so 等。本文后续的编译环境就需要依赖于我们此处编译出来的库文件。应该注意的是,此处编译出来的库文件包含对调试的支持,体积较大,在应用程序发布时,可以去掉这些调试支持,使用下列重新编译库:

# 创建非 debug 模式的库
 make BUILD_OPT=1 -f Makefile.ref

Windows 及其他平台的编译此处不再赘述,读者可以自行参考 SpiderMonkey 的官方文档。
JavaScript 对象与 C 对象间的转换关系
JavaScript 是一门弱类型的语言,变量的值的类型在运行时才确定,而且可以在运行时被修改为其他类型的变量;而 C 语言,是一门静态类型的语言,变量类型在编译时就已经确定。因此,这两者之间变量的互访就有了一定的难度,SpiderMonkey 提供了一个通用的数据类型 jsval 来完成两者之间的交互。
事实上,在 C 代码中定义的 jsval 类型的变量可以是 JavaScript 中的字符串,数字,对象,布尔值,以及 null 或者 undefined。基于这个类型,SpiderMonkey 提供了大量的类型判断及类型转换的宏和函数。可以参看下表:

表 1. JavaScript 对象与 C 对象转换表

JavaScript 类型jsval 类型判断jsval 常量jsval 转化
nullJSVAL_IS_NULL(v)JSVAL_NULL
UndefinedJSVAL_IS_VOID(v)JSVAL_VOID
BooleanJSVAL_IS_BOOLEAN(v)JSVAL_TRUE,
JSVAL_FALSE,
BOOLEAN_TO_JSVAL(b)
JSVAL_TO_BOOLEAN(v)
numberJSVAL_IS_NUMBER(v),
JSVAL_IS_INT(v),
JSVAL_IS_DOUBLE(v)
INT_TO_JSVAL(i),
DOUBLE_TO_JSVAL(d)
JSVAL_TO_INT(v),
JSVAL_TO_DOUBLE(v)
stringJSVAL_IS_STRING(v)STRING_TO_JSVAL(s)JSVAL_TO_STRING(v),
JS_GetStringChars(s),
JS_GetStringLength(s)
object
JSVAL_IS_OBJECT(v)
&& JSVAL_IS_NULL(v)
OBJECT_TO_JSVAL(o)
JSVAL_TO_OBJECT(v)
应该注意的是,jsval 有一定的缺陷:

jsval 并非完全的类型安全,在进行类型转换之前,你需要明确被转换的对象的真正类型,比如一个变量的值为 number 类型,而对其做向字符串的转化,则可能引起程序崩溃。解决方法是,在转换之前,先做判断。

jsval 是 SpiderMonkey 垃圾回收机制的主要目标,如果 jsval 引用一个 JavaScript 对象,但是垃圾收集器无法得知这一点,一旦该对象被释放,jsval 就会引用到一个悬空指针。这样很容易使得程序崩溃。解决方法是,在引用了 JavaScript 对象之后,需要显式的告知垃圾收集器,不引用时,再次通知垃圾收集器。

回页首
简单示例
基本代码模板
基本流程
使用 SpiderMonkey,一般来讲会使用以下流程:

创建运行时环境

创建一个 / 多个上下文对象

初始化全局对象

执行脚本,处理结果

释放引擎资源

在下一小节详细说明每个流程
代码模板
使用 SpiderMonkey,有部分代码是几乎每个应用程序都会使用的,比如错误报告,初始化运行时环境,上下文,全局变量,实例化全局变量等操作。这里是一个典型的模板:

清单 1. 必须包含的头文件

#include "jsapi.h"

引入 jsapi.h,声明引擎中的所用到的记号,结构体,函数签名等,这是使用 SpiderMonkey 所需的唯一一个接口文件 ( 当然,jsapi.h 中不可能定义所有的接口,这些文件在 jsapi.h 头部引入 jsapi.h,如果对 C 语言的接口,头文件引入方式不熟悉的读者,请参阅相关资料 )。

清单 2. 全局变量声明

/* 全局变量的类声明 */ 
static JSClass global_class = { 
    "global", 
    JSCLASS_GLOBAL_FLAGS, 
    JS_PropertyStub, 
    JS_PropertyStub, 
    JS_PropertyStub, 
    JS_PropertyStub, 
    JS_EnumerateStub, 
    JS_ResolveStub, 
    JS_ConvertStub, 
    JS_FinalizeStub, 
    JSCLASS_NO_OPTIONAL_MEMBERS 
 };

JSClass 是一个较为重要的数据结构,定义了 JavaScript 对象的基本结构 ---“类”,这个类可以通过 SpiderMonkey 引擎来实例化为对象。JS_PropertyStub 是 JS_PropertyOp 类型的变量,这里的 JS_PropertyStub 是为了提供一个默认值。JS_PropertyOp 可以用做对象的 setter/getter 等的,这些内容我们将在后边的章节详细讨论。

清单 3. 错误处理函数

/* 错误处理函数,用于回调,打印详细信息 */ 
 void report_error(JSContext *cx,  const char *message, JSErrorReport *report){ 
    fprintf(stderr, "%s:%u:%s/n", 
    		 report->filename ? report->filename : "<no filename>", 
            (unsigned int) report->lineno, 
            message); 
 }

定义好这些结构之后,我们需要实例化这些结构,使之成为内存对象,流程如下:

清单 4. 主流程

int main(int argc, char *argv[]){ 
    JSRuntime *runtime; 
    JSContext *context; 
    JSObject *global; 

	 // 创建新的运行时 8M 
    runtime = JS_NewRuntime(8L * 1024L * 1024L); 
    if (runtime == NULL){ 
        return -1; 
    } 

	 // 创建新的上下文
    context = JS_NewContext(runtime, 8*1024); 
    if (context == NULL){ 
        return -1; 
    } 

	 // 
    JS_SetOptions(context, JSOPTION_VAROBJFIX); 
    // 设置错误回调函数 , report_error 函数定义如上
	 JS_SetErrorReporter(context, report_error); 

	 // 创建一个新的 JavaScript 对象
    global = JS_NewObject(context, &global_class, NULL, NULL); 
    if (global == NULL){ 
        return -1; 
    } 

	 // 实例化 global, 加入对象,数组等支持
    if (!JS_InitStandardClasses(context, global)){ 
        return -1; 
    } 

	 // 
	 // 使用 global, context 等来完成其他操作,用户定制代码由此开始
	 // 

	 // 释放上下文对象
    JS_DestroyContext(context); 
	 // 释放运行时环境
    JS_DestroyRuntime(runtime); 
	 // 停止 JS 虚拟机
    JS_ShutDown(); 

	 return 0; 
 }

用户自己的代码从上边代码中部注释部分开始,用户代码可以使用此处的 context 对象及预设过一定属性,方法的 global 对象。
执行 JavaScript 代码
执行 JavaScript 代码片段
执行 JS 最简单的方式,是将脚本作为字符串交给引擎来解释执行,执行完成之后释放临时的脚本对象等。SpiderMonkey 提供一个 JS_EvaluateScript 函数,原型如下:

清单 5. 执行 JS 代码的函数原型

JSBool JS_EvaluateScript(JSContext *cx, JSObject *obj, 
    const char *src, uintN length, const
				char *filename, 
    uintN lineno, jsval *rval);

使用这个函数,需要提供上下文,全局变量,字符串形式的脚本,脚本长度及返回值指针,脚本名和行号参数可以填空值 ( 分别为 NULL 和 0)。如果函数返回 JS_TRUE,表示执行成功,执行结果存放在 rval 参数中,否则执行失败,rval 中的值为 undefined。我们可以具体来看一个例子:

清单 6. 执行 JS 代码片段

char *script = "(function(a, b){return a * b;})(15, 6);"; 
	 jsval rval; 

	 status = JS_EvaluateScript(context, global, script, strlen(script)/ 
	        , NULL, 0, &rval); 

	 if (status == JS_TRUE){ 
	    jsdouble d; 
	    JS_ValueToNumber(context, rval, &d); 
	    printf("eval result = %f/n", d); 
	 }

执行结果为:

eval result = 90.000000

编译 JavaScript 代码
通常,我们可能会多次执行一段脚本,SpiderMonkey 可以将脚本编译成 JSScript 对象,然后可以供后续的多次调用。现在来看一个例子,使用 C 代码编译一个 JavaScript 脚本,然后运行这个脚本。

清单 7. 从文件加载并执行脚本

JSBool evalScriptFromFile(JSContext *cntext, const
	char *file){ 
    JSScript *script; 
    JSString *jss; 
    JSBool status; 
    jsval value; 

    //get the global object 
    JSObject *global = JS_GetGlobalObject(context); 

    //compile the script for further using 
    script = JS_CompileFile(context, global, file); 

    if (script == NULL){ 
        return JS_FALSE; 
    } 

    //execute it once 
    status = JS_ExecuteScript(context, global, script, &value); 
    jss = JS_ValueToString(context, value); 
    printf("eval script result is : %s/n", JS_GetStringBytes(jss)); 

    //destory the script object 
    JS_DestroyScript(context, script); 

    return status; 
 }

这里传递给函数 evalScriptFromFile的 JSContext* 参数为外部创建好的 Context 对象,创建的方法参看上一节。

清单 8. 执行

JSBool status = evalScriptFromFile(context, "jstest.js"); 
 if (status == JS_FALSE){ 
        fprintf(stderr, "error while evaluate the script/n"); 
 }

假设我们将如下脚本内容保存进一个脚本 jstest.js:

清单 9. jstest.js 脚本内容

varPerson = function(name){ 
    var _name_ = name; 
    this.getName = function(){ 
        return _name_; 
    } 

    this.setName = function(newname){ 
        _name_ = newname; 
    } 
 } 

 varjack = new Person("jack"); 
 jack.setName("john"); 
 // 最后一句将作为脚本的执行结果返回给 C 代码
 jack.getName();

jack 对象的名字现在设置为了”john”, 脚本的最后一条语句的值将作为脚本的返回值返回到 C 代码处,并打印出来:

eval script result is : john

回页首
C 与 JavaScript 的交互
C 程序调用 JavaScript 函数
由于两者的数据类型上有较大的差异,因此无法直接从 C 代码中调用 JavaScript 代码,需要通过一定的转化,将 C 的变量转换为 JavaScript 可以设别的变量类型,然后进行参数的传递,返回值的处理也同样要经过转换。
我们在 JavaScript 中定义一个函数 add,这个函数接受两个参数然后返回传入的两个参数的和。定义如下:

清单 10. JavaScript 版本的 add

function add(x, y){ 
    return x + y; 
 }

然后,我们在 C 语言中根据名称调用这个 JS 函数:

清单 11. 从 C 代码中调用 JavaScript 函数

JSBool func_test(JSContext *context){ 
    jsval res; 
    JSObject *global = JS_GetGlobalObject(context); 
    jsval argv[2]; 

    //new 2 number to pass into the function "add" in script 
    JS_NewNumberValue(context, 18.5, &res); 
    argv[0] = res; 
    JS_NewNumberValue(context, 23.1, &res); 
    argv[1] = res; 

    JS_CallFunctionName(context, global, "add", 2, argv, &res); 

    jsdouble d; 

    //convert the result to jsdouble 
    JS_ValueToNumber(context, res, &d); 
    printf("add result = %f/n", d); 

    return JS_TRUE; 
 }

这里需要注意的是,JS_CallFunctionName 函数的参数列表:

清单 12. JS_CallFunctionName 原型

JSBool  JS_CallFunctionName(JSContext *cx, JSObject *obj, 
 const char *name, uintN argc, jsval *argv, jsval *rval);

表 2. JS_CallFunctionName 参数列表含义

名称类型类型描述
cx
JSContext *
上下文定义
obj
JSObject *
调用该方法的对象
name
const char *
函数名
argc
uintN
函数参数个数
argv
jsval *
函数实际参数形成的数组
rval
jsval *
返回值
参数中的 argv 是一个 jsval 形成的数组,如果直接传递 C 类型的值,则很容易出现 core dump(Linux 下的段错误所导致 ),因此,需要 JS_NewNumberValue 函数转换 C 语言的 double 到 number( 原因见对象转换小节 )。
JavaScript 程序调用 C 函数
从 JS 中调用 C 函数较上一节为复杂,我们来看一个较为有趣的例子:SpiderMonkey 中原生的 JavaScript 的全局变量中没有 print 函数,我们可以使用 C 的 printf 来实现这个功能。我们定义了一个函数 print, print 使用 logging 函数,而 logging 函数是定义在 C 语言中的,接受一个字符串作为参数,打印这个字符串到标准输出上 :

清单 13. JavaScript 调用 C 函数

//log user log in information 
 logging("user jack login on 2010/7/6"); 

 //user do nothing else 
 nothing(); 

 //log user log out information 
 logging("user jack logout on 2010/7/7"); 

 function print(){ 
    for (vari = 0; i < arguments.length; i++){ 
        logging(arguments[i]); 
    } 
 } 

 print("hello", "all", "my", "friend");

在 C 语言中,我们定义 logging 函数和 nothing 函数的原型如下:

清单 14. C 函数的实现

/** 
 * define an exposed function to be used in scripts 
 * print out all the incoming arguments as string. 
 */ 
 static JSBool  logging(JSContext *context, JSObject *object, uintN argc, 
        jsval *argv, jsval *value){ 
    int i = 0; 
    JSString *jss; 

    for(i = 0; i < argc; i++){ 
       jss = JS_ValueToString(context, argv[i]); 
       printf("message from script environment : %s/n", / 
               JS_GetStringBytes(jss)); 
    } 
    return JS_TRUE; 
 } 

 /** 
 * define an exposed function to be used in scripts 
 * do nothing but print out a single line. 
 */ 
 static JSBool  nothing(JSContext *context, 
 JSObject *object, uintN argc, jsval *argv, jsval *value) 
 { 
    printf("got nothing to do at all/n"); 
    return JS_TRUE; 
 }

从函数的签名上可以看出,C 中暴露给 JS 使用的函数,参数的个数,及对应位置上的类型,返回值都是固定的。所有的从 C 中暴露给 JS 的函数都需要“实现这个接口”。
定义好了函数之后,还需要一些设置才能在 JS 中使用这些函数。首先定义一个 JSFunctionSpec 类型的数组,然后通过 JS_DefineFunctions 将这些函数放到 global 对象上,然后在 JS 代码中就可以访问上边列出的 C 函数了。具体步骤如下:

清单 15. 注册函数列表

static JSFunctionSpec functions[] = { 
    {"logging", logging, LOG_MINARGS, 0, 0}, 
    {"nothing", nothing, NOT_MINARGS, 0, 0}, 
    {0, 0, 0, 0, 0} 
 }; 

    //define function list here 
    if (!JS_DefineFunctions(context, global, functions)){ 
        return -1; 
 }

运行结果如下:

message from script environment : user jack login on 2010/7/6 
 got nothing to do at all 
 message from script environment : user jack logout on 2010/7/7 
 message from script environment : hello 
 message from script environment : all 
 message from script environment : my 
 message from script environment : friend

在 C 程序中定义 JavaScript 对象
在 SpiderMonkey 中,在 JavaScript 中使用由 C 语言定义的对象较为复杂,一旦我们可以定义对象,使得两个世界通过 JS 交互就变得非常简单而有趣,很容易使用这样的方式来定制我们的应用,在系统发布之后仍然可以轻松的修改系统的行为。
首先,我们要定义好基本的数据结构,即我们要暴露给 JS 世界的对象的属性,结构;然后,使用 JSAPI 定义这个对象的属性;然后,使用 JSAPI 定义对象的方法;最后,创佳这个对象,并绑定其属性表和方法列表,放入全局对象。
假设我们有这样一个数据结构,用来表述一个人的简单信息 :

清单 16. PersionInfo 结构

typedef struct{ 
    char name[32]; 
    char addr[128]; 
 }PersonInfo;

定义属性表为枚举类型:

清单 17. 属性表

enum person{ 
    NAME, 
    ADDRESS
 };

我们需要将 C 语言原生的数据结构定义为 JSAPI 所识别的那样:

清单 18. 属性定义

//define person properties 
 JSPropertySpec pprops[] = { 
    {"name", NAME, JSPROP_ENUMERATE}, 
    {"address", ADDRESS, JSPROP_ENUMERATE}, 
    {0} 
 };

清单 19. 方法定义

//define person methods 
 JSFunctionSpec pfuncs[] = { 
    {"print", printing, 0}, 
    {"getName", getName, 0}, 
    {"setName", setName, 0}, 
    {"getAddress", getAddress, 0}, 
    {"setAddress", setAddress, 0}, 
    {0} 
 };

清单 20. 类定义

//define person class (JSClass) 
 JSClass pclass = { 
    "person", 0, 
    JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, 
    JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub 
 };

一旦这些基本信息定义好 (pfuncs 数组中的 getter/setter 的实现比较简单,这里由于篇幅不列出代码,感兴趣的朋友可以参看附录 ),我们就可以实例化它,并将其放入上下文中,使得 JS 代码可以访问。

清单 21. 定义对象及对象的属性,方法

JSObject *person; 

    //define the object 
    person = JS_DefineObject(/ 
            context, global, "person", &pclass, 0, JSPROP_ENUMERATE); 

    //install the properties and methods on the person object 
    JS_DefineProperties(context, person, pprops); 
    JS_DefineFunctions(context, person, pfuncs);

这样,在 JavaScript 代码中,我们就可以通过 person 这个标识符来访问 person 这个对象了:

清单 22. 测试脚本

//undefined of course 
 person.print(); 

 //person.name = "abruzzi"; 
 //person.address = "Huang Quan Road"; 

 person.setName("Desmond"); 
 person.setAddress("HuangQuan Road"); 

 //print is global function, access properties directly 
 print("person name = " + person.name); 
 print("person address = " + person.address); 

 person.print(); 

 (function(){ 
    //using getter/setter to access properties 
    return person.getName() + " : " + person.getAddress(); 
 })();

对运行结果如下:

name : undefined
 address : undefined
 person name = Desmond 
 person address = HuangQuan Road 
 name : Desmond 
 address : HuangQuan Road 
 eval script result is : Desmond : HuangQuan Road

回页首
结束语
本文中详细讨论了如何使用基于 C 的 JavaScript 引擎:SpiderMonkey 的用法。包括最基本的代码模板,C 代码与 JavaScript 代码之间的交互,以及在 C 代码中定义 JavaScript 对象等内容,使用这些基本概念,很容易将实现应用程序的脚本化。在实际的应用中,可以将应用程序的部分组件 ( 提供给用户自定义的组件 ) 暴露给 JavaScript 访问,或者在 JavaScript 脚本中提供函数的存根 ( 仅仅定义函数的原型 ),用户可以通过实现这些函数的具体逻辑,来实现脚本化。


回页首
下载

描述名字大小下载方法
样例代码jsfun.zip8KBHTTP
关于下载方法的信息


参考资料
学习

SpiderMonkey 官方网站,包括 API 函数签名,完整的文档等。

w3c 的一个简易的 JavaScript教程,介绍客户端的 JavaScript 的基本用法。

一篇介绍 SpiderMonkey 的文章,介绍 SpiderMonkey 基本的用法。

developerWorks Linux 专区寻找为 Linux 开发人员(包括 Linux 新手入门)准备的更多参考资料,查阅我们 最受欢迎的文章和教程
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: