利用v8引擎实现运行js文件
这篇文章接着上篇文章《V8的编译实战》,相信大家根据上篇文章非常简单的实现了v8的编译,这篇文章主要是讲如何基于 利用v8引擎实现运行js文件 。相信从这篇文章之后,大家能认识到实现一个简单的js运行时,并不是很难。每次都讲helloworld,没什么意义,这次参考 shell。cc,写一个js代码执行器。
准备 && 配置
上一篇本人的v8源码的路径是/home/zy445566/v8,所以依旧基于这个目录完成代码的构建
# 创建自己的代码目录
mkdir -p /home/zy445566/v8/zy_node_src
# 创建一个用于写代码的路径
touch /home/zy445566/v8/zy_node_src/zy_node.cc
但我们要如何在ninja是如何关联编译的呢?因为最新版本的v8默认是是基于gn构建的,所以我们只要修改v8源码目录的BUILD.gn来配置编译关联就行了,编辑v8目录的BUILD.gn并在末尾加上以下代码就好,说明请看注释。OK,那么我们来配置gn吧。
# 这行表示我们要输出的文件名
v8_executable("zy_node") {
# 这里表示要编译的代码
sources = [
# 这个文件就是我们之前touch的文件
"zy_node_src/zy_node.cc",
# 由于原生的console输出方式默认不向命令行中输出
# 所以我们使用d8的console用于命令的输出
# 这也是我们要编译的文件
"src/d8-console.cc",
"src/d8-console.h",
]
# 默认的配置
configs = [
":internal_config_base",
]
# 这里表示我依赖的组件
deps = [
":v8",
":v8_libbase",
":v8_libplatform",
"//build/win:default_exe_manifest",
]
}
OK,配置上基本是完事了,我们开始直接开始写代码了
编码
引入文件和头
写在前面,源码请直接参考zy_node.cc 首先讲解引用,这是代码的头部,因为我的文件并不是很大,所以头文件并没有拆开 以下主要是引用文件和定义的头文件,详情请看注释
#include <include/v8.h>
#include <include/libplatform/libplatform.h>
#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//因为引用了d8-console,所以在BUILD.gn配置里面加入了需要编译
#include "src/d8-console.h"
// 这里就是一个创建上下问的方法,我只需要一个上下文,你可以简单理解为定义全局变量
v8::Local<v8::Context> CreateContext(v8::Isolate* isolate);
// 这里就是用于执行命令行的文件的功能
int RunMain(v8::Isolate* isolate, v8::Platform* platform, int argc,
char* argv[]);
//用于执行js字符串的功能
bool ExecuteString(v8::Isolate* isolate, v8::Local<v8::String> source,
v8::Local<v8::Value> name, bool print_result,
bool report_exceptions);
//内嵌到js代码里面的功能,你可以理解为fs.readFileSync
void ReadFile(const v8::FunctionCallbackInfo<v8::Value>& args);
v8::MaybeLocal<v8::String> ReadCommandFile(v8::Isolate* isolate, const char* name);
//用于异常的打印
void ReportException(v8::Isolate* isolate, v8::TryCatch* handler);
入口文件
因为C语言的入口就是main方法,我们可以仔细看一下main方法,并详细解析
基本概念(这里我就用简单的白话说说):
- Isolate:隔离层,你可以理解为nodejs的vm,你创建一个vm就是一个Isolate,里面可以有多个上下文,多个vm可以互相隔离
- Context:上下文,举个例子,一个方法你除了拿方法内的变量数据,你还可以拿方法外的变量数据,而方法外的数据载体就是上下文,简单的理解就是全局变量的载体。
- Handle: 可以理解为js的对象的变量名,但本质是一个对象数据的指针
- HandleScope: 是一个用于装Handle的杯子,而实际是一个栈,如果Handle出了这个最终的HandleScope,意味这个这个数据要从Heap树中移除,等待回收(Scope可以类比到上下文和隔离层)
int main(int argc, char* argv[]) {
// 初始化引擎和数据的装载
v8::V8::InitializeICUDefaultLocation(argv[0]);
v8::V8::InitializeExternalStartupData(argv[0]);
// 初始化平台数据
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform.get());
// v8初始化
v8::V8::Initialize();
// 创建隔离层的参数,包括使用的内存开辟器
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator =
v8::ArrayBuffer::Allocator::NewDefaultAllocator();
// 创建隔离
v8::Isolate* isolate = v8::Isolate::New(create_params);
// 这里使用了D8的console来替换默认的console
v8::D8Console console(isolate);
v8::debug::SetConsoleDelegate(isolate, &console);
int result;
{
// 将isolate放入isolate_scope槽中
v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);
// 创建一个上下文
// CreateContext就是将ReadFile绑定到全局变量中的方法
// CreateContext在稍后会讲到
v8::Local<v8::Context> context = CreateContext(isolate);
if (context.IsEmpty()) {
fprintf(stderr, "Error creating context\n");
return 1;
}
// 将上下文放入scope中
v8::Context::Scope context_scope(context);
// 捕获命令行中的文件名的参数,并运行
result = RunMain(isolate, platform.get(), argc, argv);
}
// 各种销毁,就不讲了
isolate->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete create_params.array_buffer_allocator;
return result;
}
如何在js中嵌入一个全局的自己写的C++方法
接下来讲解main中的CreateContext方法: 之前也讲解到Context是一个相对全局数据的载体,那么这个方法就是让一些自己写的C++的方法能够绑定到这个载体上,让全局都能使用这个载体,比如nodejs的require就是一个全局对象 我这里增加了一个ReadFile的方法到全局变量中,让自己的运行时的js支持读取本地文件 讲CreateContext前,先讲ReadFile和ReadCommandFile的实现,而CreateContext仅仅是把这个方法绑定到全局而已
// 是不是很像nodejs的C++扩展的写法
void ReadFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
// 判断参数是不是只有一个
if (args.Length() != 1) {
args.GetIsolate()->ThrowException(
v8::String::NewFromUtf8(args.GetIsolate(), "Bad parameters",
v8::NewStringType::kNormal).ToLocalChecked());
return;
}
// 将文件名转换成v8的字符串
v8::String::Utf8Value file(args.GetIsolate(), args[0]);
if (*file == NULL) {
args.GetIsolate()->ThrowException(
v8::String::NewFromUtf8(args.GetIsolate(), "Error loading file",
v8::NewStringType::kNormal).ToLocalChecked());
return;
}
// 定义文件里面的代码数据的变量
v8::Local<v8::String> source;
//调用ReadCommandFile读出文件代码,并检查是否读取成功,ReadCommandFile方法后面会讲到
if (!ReadCommandFile(args.GetIsolate(), *file).ToLocal(&source)) {
args.GetIsolate()->ThrowException(
v8::String::NewFromUtf8(args.GetIsolate(), "Error loading file",
v8::NewStringType::kNormal).ToLocalChecked());
return;
}
// 将文件的代码数据返回
args.GetReturnValue().Set(source);
}
// 这个就是根据文件名读数据,并转换成v8的字符串格式并返回,这个略微带过,稍微有点C基础都很容易看懂
v8::MaybeLocal<v8::String> ReadCommandFile(v8::Isolate* isolate, const char* name) {
FILE* file = fopen(name, "rb");
if (file == NULL) return v8::MaybeLocal<v8::String>();
fseek(file, 0, SEEK_END);
size_t size = ftell(file);
rewind(file);
char* chars = new char[size + 1];
chars[size] = '\0';
for (size_t i = 0; i < size;) {
i += fread(&chars[i], 1, size - i, file);
if (ferror(file)) {
fclose(file);
return v8::MaybeLocal<v8::String>();
}
}
fclose(file);
// 这个就是把char转换成v8的字符串数据,实际上这是一个Handle
v8::MaybeLocal<v8::String> result = v8::String::NewFromUtf8(
isolate, chars, v8::NewStringType::kNormal, static_cast<int>(size));
delete[] chars;
return result;
}
//CreateContext这个方法其实就三行
v8::Local<v8::Context> CreateContext(v8::Isolate* isolate) {
// 新建一个js的Object,Template其实就是C++版本的对象的意思
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);
// 将这个对象绑定一个方法,这个方法在js里面叫readfile,绑定的方法就是上面讲的ReadFile
// FunctionTemplate和上面Template可以类推
global->Set(
v8::String::NewFromUtf8(isolate, "readfile", v8::NewStringType::kNormal)
.ToLocalChecked(),
v8::FunctionTemplate::New(isolate, ReadFile));
// 把上下文初始化出来
return v8::Context::New(isolate, NULL, global);
}
如何真正通过命令行实现运行js脚本
重头戏来了 之前讲过入口文件,最后运行RunMain来运行js文件,接下来,就详细讲一下它是如何做到的。
列一下出现方法
- RunMain (js的具体运行流程)
- ExecuteString (具体执行js代码的功能)
- ReportException(报告js错误的方法,这里不细讲,有兴趣自己研究)
- ReadCommandFile (这个上节讲过的来读取文件,并转换成v8的字符串类型)
int RunMain(v8::Isolate* isolate, v8::Platform* platform, int argc,
char* argv[]) {
// 这里取出了命令行的第二个参数,为什么是第二个?
// ./zy_node test.js 因为第一个是它本身啊
const char* str = argv[1];
// 将字符串转成v8的类型
v8::Local<v8::String> file_name =
v8::String::NewFromUtf8(isolate, str, v8::NewStringType::kNormal)
.ToLocalChecked();
// 定义js文件内容的变量
v8::Local<v8::String> source;
// 用ReadCommandFile读取文件到source,并判断是否读取异常
if (!ReadCommandFile(isolate, str).ToLocal(&source)) {
fprintf(stderr, "Error reading '%s'\n", str);
return 0;
}
// 开始执行字符串用js引擎,可以直接往下看到ExecuteString方法
bool success = ExecuteString(isolate, source, file_name, false, true);
// 这个就是从事件循环(大名顶顶的EVENT_LOOP)中,执行异步函数
while (v8::platform::PumpMessageLoop(platform, isolate)) continue;
// 如果失败返回1
if (!success) return 1;
return 0;
}
bool ExecuteString(v8::Isolate* isolate, v8::Local<v8::String> source,
v8::Local<v8::Value> name, bool print_result,
bool report_exceptions) {
// 这就是我之前说的“杯子”,不解释第二次了,往上看
v8::HandleScope handle_scope(isolate);
// 外面包个try...catch
v8::TryCatch try_catch(isolate);
// 将js文件文件名传入,初始化脚本
v8::ScriptOrigin origin(name);
// 直接拿当前隔离层的上下文,作为当前的上下文
v8::Local<v8::Context> context(isolate->GetCurrentContext());
// 定义脚本变量
v8::Local<v8::Script> script;
// 编译文件代码,成功后并传入script变量中,异常则报错
if (!v8::Script::Compile(context, source, &origin).ToLocal(&script)) {
// report_exceptions 是传入的值,用于报错的开关
if (report_exceptions)
ReportException(isolate, &try_catch);
return false;
} else {
// 定义执行结果
v8::Local<v8::Value> result;
// 运行之前编译成功的script,如果运行失败则报错
//!!!!注意这行代码就是在运行了
if (!script->Run(context).ToLocal(&result)) {
assert(try_catch.HasCaught());
// Print errors that happened during execution.
if (report_exceptions)
ReportException(isolate, &try_catch);
return false;
} else {
//判断确实没有错误,则往下运行
assert(!try_catch.HasCaught());
// 是否打印结果,print_result也是一个传入的开关
if (print_result && !result->IsUndefined()) {
// If all went well and the result wasn't undefined then print
// the returned value.
v8::String::Utf8Value str(isolate, result);
const char* cstr = ToCString(str);
printf("%s\n", cstr);
}
return true;
}
}
}
OK!代码讲解完了,就走编译步骤了!
编译
因为我们开篇就进行了配置BUILD。gn,所以这次我们直接编译就好 这次编译步骤基本和我上篇文章讲的走
cd v8
ninja -C out.gn/x64.release
不得不说gn的增量编译真的爽,之前编译过的不需要二次编译了。再也不想回到过去的gyp时代了。
运行
先写一个js文件在/home/zy445566/v8/out.gn/x64.release/test.js目录下,将以下的代码
// 测试自己写的js运行时,是否正常运行
var a = 1;
var b = 2;
console.log(3);
console.log("test.js:");
// 检查之前写的C++方法ReadFile是否按readfile是否注入
// 是否readfile能正常打印自己
console.log(readfile("test.js"));
看看运行结果: 完美!!!
总结
走完这些步骤,相信只要花足够多的时间堆足够的C++组建就可以模仿出一个简单的node.js。但是我认为直接的基于v8开放不一定是好事,v8本质上就是一个js引擎是一个单独的组件,不应该直接基于v8在上面深度的定制和大量写出和v8底层相关的代码。但由于js引擎没有开发开放标准,导致后面的开发者极度容易直接基于v8直接定制,而不是和引擎接入标准挂钩,这样就容易随着开发定制版时间的推移v8本身的api就越来越难移植到自己的运行时中。v8这点应该学习Rkt,做一个开放和中立的js引擎标准。有兴趣的同学也可以想想如何改变。
等更新
请问 Rtk 是什么?
@zurmokeeper 不好意思打快了,这个是Rkt,已修正
@zy445566 Rkt是那个CoreOS推出的容器引擎吧,您把它和V8对比,是为了说明它的什么优点呢?
@zurmokeeper 主要是rkt开放了标准应用容器规范,我希望v8也应该有一个类似的js引擎接口开放标准
@zy445566 js引擎接口开放标准是指使用V8开发的时候,能有个大概的标准,比如怎么使用,环境变量命名之类的一系列规范吗?
@zurmokeeper 不是的,因为V8的底层接口会经常变动,假设你获取系统快照的方法原来名字是getSnapshot后来又改为了getHotSnapshot,如果原来调用了getSnapshot,如果接口名字改了,你基于V8的上层接口是不是要发生改变。NAN和n-api实际上都是为了兼容V8底层接口的改动而来的,如果有标准不变,这两个的意义是不是减小了。同时假设我不是使用C++来接入,是否能用其它语言接入V8的开发,这都需要标准。
@zy445566 原来是这样啊,现在接入V8开发是不是只能用C++和JS?
@zurmokeeper 请问有在WIN上编译过吗?
@zurmokeeper 没有,推荐你装个ubtuntu的14.04的虚拟机,
@zy445566 哈哈,麻烦您回下另外一个问题?
@zurmokeeper 目前只能用C++和JS,而且JS主要是辅助实现一些功能,但本质还是接入在C++构造的平台上
@zy445566 谢谢解答了,接下来就是期待您的更新了,希望能快点看到。