逐渐有些原生语言项目(比如c++)因为希望有不停机更新的能力而引入脚本。这些团队往往有一套成熟c++服务器框架,他们往往选择把脚本作为库嵌入到C++程序的做法。
服务器选用一个库,最看重的莫过于稳定性和性能了,在众多脚本引擎中,v8这两方面可谓佼佼者:
稳定性源自长时间各种方式的折腾,v8引擎每天那么多的实例跑在各种各样的机器、环境下,跑着各种各样的代码,一天跑的代码量比很多小众的脚本引擎一辈子的代码量还多,而且nodejs的应用也验证了v8跑在服务器环境是没问题的。
性能这块,在jit的加持下,虽说比不上原生语言,但在脚本中肯定是第一档的存在。
对于c++程序猿,v8还有个很诱人的地方,wasm的支持,c++编译成wasm在v8上跑,性能比js还能高一个台阶,而且还能热更新。
v8引擎看上去很合适服务器使用,目前却很少项目应用到游戏服务器上,一些项目交流说有过这样的想法,但不知道怎么做v8嵌入。于是有了本文,本文会循序渐进的介绍怎么在linux c++程序里头嵌入v8:
上述三步都会附带完整的可运行代码,最后会附上github仓库链接。
直接上王道
//...各种include
// -------------------------begin 1-----------------------------
static void Print(const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::Isolate* isolate = info.GetIsolate();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
std::string msg = *(v8::String::Utf8Value(isolate, info[0]->ToString(context).ToLocalChecked()));
std::cout << msg << std::endl;
}
// -------------------------end 1-----------------------------
int main(int argc, char* argv[]) {
// -------------------------begin 2-----------------------------
// Initialize V8.
v8::StartupData SnapshotBlob;
SnapshotBlob.data = (const char *)SnapshotBlobCode;
SnapshotBlob.raw_size = sizeof(SnapshotBlobCode);
v8::V8::SetSnapshotDataBlob(&SnapshotBlob);
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform.get());
v8::V8::Initialize();
// Create a new Isolate and make it the current one.
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator =
v8::ArrayBuffer::Allocator::NewDefaultAllocator();
v8::Isolate* isolate = v8::Isolate::New(create_params);
// -------------------------end 2-----------------------------
{
// -------------------------begin 3-----------------------------
v8::Isolate::Scope isolate_scope(isolate);
// Create a stack-allocated handle scope.
v8::HandleScope handle_scope(isolate);
// Create a new context.
v8::Local<v8::Context> context = v8::Context::New(isolate);
// Enter the context for compiling and running the hello world script.
v8::Context::Scope context_scope(context);
// -------------------------end 3-----------------------------
// -------------------------begin 4-----------------------------
context->Global()->Set(context, v8::String::NewFromUtf8(isolate, "Print").ToLocalChecked(),
v8::FunctionTemplate::New(isolate, Print)->GetFunction(context).ToLocalChecked())
.Check();
// -------------------------end 4-----------------------------
{
// -------------------------begin 5-----------------------------
const char* csource = R"(
Print('hello world');
)";
// Create a string containing the JavaScript source code.
v8::Local<v8::String> source =
v8::String::NewFromUtf8(isolate, csource)
.ToLocalChecked();
// Compile the source code.
v8::Local<v8::Script> script =
v8::Script::Compile(context, source).ToLocalChecked();
// Run the script
auto _unused = script->Run(context);
}
// -------------------------end 5-----------------------------
}
// -------------------------begin 6-----------------------------
// Dispose the isolate and tear down V8.
isolate->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete create_params.array_buffer_allocator;
return 0;
// -------------------------end 6-----------------------------
}
以上一大堆代码最终运行效果只是打印了个“hello world”,没接触过的童靴是不是有点晕菜,别急,有我。
上述代码我用分割线分成了6块,其中:
ps,v8的api在本文就不多介绍了,网上有很多资料可以学习,比如这篇:https://github.com/danbev/learning-v8
上述例子演示了怎么去启动一个脚本,以及怎么从脚本调用原生。在Print只是简单的取一个参数进行打印,如果有更多个数及种类的参数呢?更复杂的是一个c++类有构造函数,成员变量,有成员函数,静态函数,还有继承,重载等等,c++类如果需要封装不是十分麻烦?
这就轮到puerts出场了,为服务器童鞋科普下:puerts最初是Unreal Engine、Unity游戏引擎下的typescript编程解决方案,但游戏引擎以外的环境也逐步在支持,其中任意C#环境早已支持,而c++ 11以上环境,最近也加入支持之列。通过puerts,我们仅仅只需对c++进行些声明操作,即可被js使用,甚至可以根据c++ api生成.d.ts文件。
用个比较简单又有一定代表性的c++类为例:
class TestClass
{
public:
TestClass(int p) {
std::cout << "TestClass(" << p << ")" << std::endl;
X = p;
}
static void Print(std::string msg) {
std::cout << msg << std::endl;
}
int Add(int a, int b)
{
std::cout << "Add(" << a << "," << b << ")" << std::endl;
return a + b;
}
int X;
};
对上述类,只需要在c++里头做如下声明:
UsingCppType(TestClass);
int main(int argc, char* argv[]) {
//other...
//注册
puerts::DefineClass<TestClass>()
.Constructor<int>()
.Function("Print", MakeFunction(&TestClass::Print))
.Property("X", MakeProperty(&TestClass::X))
.Method("Add", MakeFunction(&TestClass::Add))
.Register();
//other...
}
然后就能在js里头使用(ps,puerts还支持对上述类生成typescript类型定义):
const TestClass = loadCppType('TestClass');
TestClass.Print('hello world');
let obj = new TestClass(123);
TestClass.Print(obj.X);
obj.X = 99;
TestClass.Print(obj.X);
TestClass.Print('ret = ' + obj.Add(1, 3));
当然,要支持这些,还需要对puerts做一定的初始化操作,在这就不再赘述,各位可于文后链接获取代码,对比第一版Helloworld即可得知用法。
至此,我们能在c++程序里执行js代码, js能调用到c++代码。这对一些项目已经足够了。
不过我们嵌入的v8引擎,只实现了es规范语法以及api,像setTimeout这种耳熟能详的api,都不是es规范的内容,其次有的项目组希望能对接npm上丰富的组件,那有没可能往c++程序嵌入一个nodejs呢?请看下一章节。
这是nodejs这方面的官方文档:https://nodejs.org/dist/latest-v14.x/docs/api/embedding.html
官方也给了个简单的示例:https://github.com/nodejs/node/blob/HEAD/test/embedding/embedtest.cc
我们先尝试把官方的例子跑起来,第一步我们要编译libnode.so,下载或者clone node源码,进入源码目录,执行如下命令:
./configure --shared
make -j4
漫长的编译完成后,会在out/Release/下找到libnode.so.95文件,这就是动态库版本的node,接下来编译官方的嵌入式例子
cd test/embedding
c++ -I../../src -I../../deps/v8/include -I../../deps/uv/include -c embedtest.cc
c++ embedtest.o -Wl,-rpath,../../out/Release ../../out/Release/libnode.so.95
跑一下试试
./a.out "console.log('hello world')"
跟着,我们把上一章节的TestClass,Puerts加入到这程序,然后在js里试试看?
const TestClass = loadCppType('TestClass');
TestClass.Print('hello world');
let obj = new TestClass(123);
TestClass.Print(obj.X);
obj.X = 99;
TestClass.Print(obj.X);
TestClass.Print('ret = ' + obj.Add(1, 3));
const fs = require('fs');
let info = fs.readdirSync('.');
console.log(info);
除了之前的c++类调用之外,还加了nodejs api的调用,以证明这确实是个完整的nodejs环境。
nodejs的嵌入可能要了解的情况更多,它内部有一套事件循环处理逻辑,也会启动些线程,要注意这些是否和原来的服务器框架有冲突。相比之下,上一章节的纯v8环境只是一个库,它跑不跑取决于你是否调用,会简单得多。
就介绍那么多,附上完整的实例代码以及编译配置,按readme操作就可以运行: