使用 V8 - Google 的 Chrome JavaScript 虚拟机
教程和示例,解释如何在您的应用程序中使用 V8 虚拟机。
引言
谁不曾好奇虚拟机是如何工作的?但是,与其自己创建一个虚拟机,不如使用一家大型公司开发并专注于提高虚拟机速度的虚拟机?在这篇文章中,我将介绍如何在您的应用程序中使用 V8,它是 Chrome(谷歌的新浏览器)中谷歌的开源 JavaScript 引擎。
背景
这段代码将 V8 用作嵌入式库来执行 JavaScript 代码。要获取库的源代码和更多信息,请访问 V8 开发者页面。要有效地使用 V8 库,您需要了解 C/C++ 和 JavaScript。
Using the Code
让我们看看演示中有什么。演示展示了
- 如何使用 V8 库 API 执行 JavaScript 源代码。
- 如何访问脚本中的整数和字符串。
- 如何创建自己的函数,该函数可以在脚本中调用。
- 如何创建自己的 C++ 类,该类可以在脚本中调用。
首先,让我们了解如何初始化 API。请看这个在 C++ 中嵌入 V8 的简单示例
#include <v8.h>
using namespace v8;
int main(int argc, char* argv[]) {
// Create a stack-allocated handle scope.
HandleScope handle_scope;
// Create a new context.
Handle<Context> context = Context::New();
// Enter the created context for compiling and
// running the hello world script.
Context::Scope context_scope(context);
// Create a string containing the JavaScript source code.
Handle<String> source = String::New("'Hello' + ', World!'");
// Compile the source code.
Handle<Script> script = Script::Compile(source);
// Run the script to get the result.
Handle<Value> result = script->Run();
// Convert the result to an ASCII string and print it.
String::AsciiValue ascii(result);
printf("%s\n", *ascii);
return 0;
}
好的,但这并没有解释我们如何控制脚本中的变量和函数。好吧,脚本会做一些事情……但是,我们需要以某种方式获取这些信息,并拥有自定义函数来控制脚本内的特定行为。
全局模板
首先,我们需要一个全局模板来控制我们的修改
v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
这会创建一个新的全局模板,它管理我们的上下文和我们的自定义项。这很重要,因为在 V8 中,每个上下文都是独立的,并且可以拥有自己的全局模板。在 V8 中,上下文是一个执行环境,它允许独立的、不相关的 JavaScript 应用程序在单个 V8 实例中运行。
添加自定义函数
之后,我们可以添加一个名为“plus
”的新函数
// plus function implementation - Add two numbers
v8::Handle<v8::Value> Plus(const v8::Arguments& args)
{
unsigned int A = args[0]->Uint32Value();
unsigned int B = args[1]->Uint32Value();
return v8_uint32(A + B);
}
//...
//associates plus on script to the Plus function
global->Set(v8::String::New("plus"), v8::FunctionTemplate::New(Plus));
自定义函数需要接收 const v8::Arguments&
作为参数,并且必须返回 v8::Handle<v8::Value>
。使用之前创建的全局模板指针,我们将函数添加到模板中,并将名称“plus
”与回调“Plus
”关联起来。现在,每次我们在脚本中使用“plus
”函数时,都会调用“Plus
”函数。Plus
函数只做一件事:获取第一个和第二个参数并返回它们的和。
好的,现在我们可以在 JavaScript 端使用自定义函数了
plus(120,44);
并且我们可以在脚本中使用此函数的结果
x = plus(1,2);
if( x == 3){
// do something important here!
}
访问器 - 脚本内可访问的变量!
现在,我们可以创建函数……但如果我们可以使用脚本外部定义的某些内容,那不是更酷吗?我们来试试! V8 有一个称为 Accessor 的东西,通过它,我们可以将一个变量与一个名称和两个 Get/Set 函数关联起来,V8 在运行脚本时会使用它们来访问该变量。
global->SetAccessor(v8::String::New("x"), XGetter, XSetter);
这会将名称“x
”与“XGetter
”和“XSetter
”函数关联起来。“XGetter
”将在脚本需要“x
”变量的值时由 V8 调用,而“XSetter
”将在脚本必须更新“x
”的值时由 V8 调用。现在,函数
//the x variable!
int x;
//get the value of x variable inside javascript
static v8::Handle<v8::Value> XGetter( v8::Local<v8::String> name,
const v8::AccessorInfo& info) {
return v8::Number::New(x);
}
//set the value of x variable inside javascript
static void XSetter( v8::Local<v8::String> name,
v8::Local<v8::Value> value, const v8::AccessorInfo& info) {
x = value->Int32Value();
}
在 XGetter
中,我们只需要将“x
”转换为 V8 管理的数字类型值。在 XSetter
中,我们需要将作为参数传递的值转换为整数值。每种基本类型都有一个函数(NumberValue
用于 double
,BooleanValue
用于 bool
等)
现在,我们可以为字符串(char*
)做同样的事情
//the username accessible on c++ and inside the script
char username[1024];
//get the value of username variable inside javascript
v8::Handle<v8::Value> userGetter(v8::Local<v8::String> name,
const v8::AccessorInfo& info) {
return v8::String::New((char*)&username,strlen((char*)&username));
}
//set the value of username variable inside javascript
void userSetter(v8::Local<v8::String> name, v8::Local<v8::Value> value,
const v8::AccessorInfo& info) {
v8::Local<v8::String> s = value->ToString();
s->WriteAscii((char*)&username);
}
对于字符串,情况稍有变化。“userGetter
”以与 XGetter
相同的方式创建一个新的 V8 字符串,但 userSetter
需要首先通过使用 ToString
函数访问内部字符串缓冲区。然后,使用指向内部字符串对象的指针,我们使用 WriteAscii
函数将内容写入我们的缓冲区。现在,只需添加访问器,即可大功告成!
//create accessor for string username
global->SetAccessor(v8::String::New("user"),userGetter,userSetter);
打印
“print
”函数是另一个自定义函数,它捕获所有参数并使用“printf
”函数打印它们。就像我们之前对“plus
”函数所做的那样,我们将新函数注册到全局模板
//associates print on script to the Print function
global->Set(v8::String::New("print"), v8::FunctionTemplate::New(Print));
“print”实现
// The callback that is invoked by v8 whenever the JavaScript 'print'
// function is called. Prints its arguments on stdout separated by
// spaces and ending with a newline.
v8::Handle<v8::Value> Print(const v8::Arguments& args) {
bool first = true;
for (int i = 0; i < args.Length(); i++)
{
v8::HandleScope handle_scope;
if (first)
{
first = false;
}
else
{
printf(" ");
}
//convert the args[i] type to normal char* string
v8::String::AsciiValue str(args[i]);
printf("%s", *str);
}
printf("\n");
//returning Undefined is the same as returning void...
return v8::Undefined();
}
对于每个参数,都会构造 v8::String::AsciiValue
对象来创建一个 char*
表示形式的值。通过它,我们可以将其他类型转换为它们的字符串表示形式并打印它们!
JavaScript 示例
在演示程序中,我们有以下 JavaScript 示例来使用我们迄今为止创建的内容
print("begin script");
print(script executed by + user);
if ( user == "John Doe"){
print("\tuser name is invalid. Changing name to Chuck Norris");
user = "Chuck Norris";
}
print("123 plus 27 = " + plus(123,27));
x = plus(3456789,6543211);
print("end script");
此脚本使用了“x
”变量、“user
”变量以及“plus
”和“print
”函数。
带 C++ 对象的访问器!
为我们的类准备环境
现在,如何使用 C++ 将类映射到 JavaScript?首先,示例类
//Sample class mapped to v8
class Point
{
public:
//constructor
Point(int x, int y):x_(x),y_(y){}
//internal class functions
//just increment x_
void Function_A(){++x_; }
//increment x_ by the amount
void Function_B(int vlr){x_+=vlr;}
//variables
int x_;
};
为了让这个类完全在 JavaScript 中可用,我们需要映射函数和内部变量。第一步是在我们的上下文中映射一个类模板
Handle<FunctionTemplate> point_templ = FunctionTemplate::New();
point_templ->SetClassName(String::New("Point"));
我们创建一个“函数”模板,但这可以被视为一个类。此模板将具有“Point
”名称,以便以后使用(例如在测试脚本中创建该类的实例)。
然后,我们访问原型类模板以向我们的类添加内置方法
Handle<ObjectTemplate> point_proto = point_templ->PrototypeTemplate();
point_proto->Set("method_a", FunctionTemplate::New(PointMethod_A));
point_proto->Set("method_b", FunctionTemplate::New(PointMethod_B));
在此之后,该类“知道”它有两个方法和回调。但这仍然在原型中,我们无法使用它,除非访问类的实例。
Handle<ObjectTemplate> point_inst = point_templ->InstanceTemplate();
point_inst->SetInternalFieldCount(1);
SetInternalFieldCount
函数为 C++ 类指针创建空间(稍后会看到)。
现在,我们有了类的实例,并且可以为内部变量添加访问器
point_inst->SetAccessor(String::New("x"), GetPointX, SetPointX);
然后,我们准备好了“地面”……撒下种子
Point* p = new Point(0, 0);
新类已创建,但只能在 C++ 中访问。要访问它,我们需要
Handle<Function> point_ctor = point_templ->GetFunction();
Local<Object> obj = point_ctor->NewInstance();
obj->SetInternalField(0, External::New(p));
好了,GetFunction
返回点构造函数(在 JavaScript“端”),通过它可以创建一个新实例使用 NewInstance
。然后,我们使用类指针设置我们的内部字段(我们已经用 SetInternalFieldCount
为其“创建了空间”)。这样,JavaScript 就可以通过指针访问该对象。
只剩最后一步了。我们只有一个类模板和实例,但没有名称可以在 JavaScript 中访问它
context->Global()->Set(String::New("point"), obj);
最后一步将“point
”名称链接到实例 obj
。通过这些步骤,我们可以在脚本中(在加载或运行它之前!)模拟创建名称为 point
的类 Point
。
在 JavaScript 中访问类方法
好的,但这并没有解释我们如何访问 Point
类中的 Function_A
……
让我们看看 PointMethod_A
回调
Handle<Value> PointMethod_A(const Arguments& args)
{
Local<Object> self = args.Holder();
Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
void* ptr = wrap->Value();
static_cast<Point*>(ptr)->Function_A();
return Integer::New(static_cast<Point*>(ptr)->x_);
}
像正常的访问器一样,我们必须处理参数。但是,要访问我们的类,我们必须从内部字段(第一个)获取类指针。在将内部字段映射到“wrap
”之后,我们通过检索其“值”来获取类指针。现在,这是转换的问题……
现在,我们有了正在使用的“point
”类的类指针(它调用了 method_a
,它调用了回调 PointMethod_A
)。
示例v8_embedded_demo_with_object.zip 实现所有这些步骤。
我希望这有帮助,如果有什么不对或不清楚的地方,请随时联系。
关注点
在尝试使用 V8 库之前,我曾尝试使用 Lua 作为嵌入式脚本语言,以支持用户轻松配置和更改。我惊叹于 V8,因为它比 Lua 更快、更易于在我的软件中实现脚本功能。我希望这篇文章能帮助其他人完成这项任务。
历史
- 2008 年 9 月 5 日 - 首次实现。
- 2008 年 9 月 8 日 - Lua 与 V8 的基准测试以及使用动态变量的示例。
- 2008 年 9 月 11 日 - Lua 基准测试已更新(第 2 版),最后,JavaScript 中的对象访问。