保姆级教程: c++游戏服务器嵌入v8 js引擎

云加社区

共 7001字,需浏览 15分钟

 ·

2021-08-19 08:24


导语 | 本文将介绍在c++游戏服务器上嵌入v8 js引擎的详细教程,关键步骤都会附带完整的可运行代码。并在文末为您附上github仓库链接。


逐渐有些原生语言项目因为希望有不停机更新的能力而引入脚本。而且由于大多数项目已经有现成的c++服务器框架,他们往往选择把脚本作为库嵌入到c++程序的做法。


服务器选用一个库,最看重的莫过于稳定性和性能了,在众多脚本引擎中,v8这两方面可谓佼佼者:稳定性源自长时间各种方式的折腾,v8引擎每天那么多的实例跑在各种各样的机器、环境下,跑着各种各样的代码,一天跑的代码量比很多小众的脚本引擎一辈子的代码量还多,而且nodejs的应用也验证了v8跑在服务器环境是没问题的。


性能这块,在jit的加持下,虽说比不上原生语言,但在脚本中肯定是第一档的存在。


对于c++程序猿,v8还有个很诱人的地方:支持wasm,c++编译成wasm在v8上跑,性能比js还能高一个台阶,而且还能热更新


v8引擎看上去很合适服务器使用,目前却很少项目应用到游戏服务器上,一些项目交流说有过这样的想法,但不知道怎么做v8嵌入。于是有了本文,本文会循序渐进的介绍怎么在linux c++程序里头嵌入v8:


  • HelloWorld级别的示例;

  • c++类封装到js;

  • 把v8改为嵌入式nodejs;


上述三步都会附带完整的可运行代码,最后会附上github仓库链接



一、HelloWorld


直接上王道:


//...各种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块其中:


  • 第2块是v8的启动,第6块是v8的关闭,除非你要定制启动参数,启动多虚拟机啥的,否则这两部分都是固定的;


  • 第1块有个Print函数,和这函数同声明的c++函数,都可以注册到js环境里头被js调用,函数只是简单的把参数取出通过std::cout输出;


  • 第4块把前面的Print函数注册到js的全局变量,名字也叫Print;


  • 第5块执行了一段js代码,调用了Print函数。


上述例子演示了怎么去启动一个脚本,以及怎么从脚本调用原生。在Print只是简单的取一个参数进行打印,如果有更多个数及种类的参数呢?更复杂的是一个c++类有构造函数,成员变量,有成员函数,静态函数,还有继承,重载等等,c++类如果需要封装不是十分麻烦?


这就轮到puerts出场了,为服务器童鞋科普下:puerts最初是Unreal Engine、Unity游戏引擎下的typescript编程解决方案,但游戏引擎以外的环境也逐步在支持,其中任意C#环境早已支持,而c++ 11以上环境,最近也加入支持之列。通过puerts,我们仅仅只需对c++进行些声明操作,即可被js使用,甚至可以根据c++ api生成.d.ts文件


二、
Powered by Puerts


用个比较简单又有一定代表性的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呢?请看下一章节。


三、Powered by embedding Nodejs


第一步我们要编译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 embedtest.cc -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操作就可以运行:

https://github.com/chexiongsheng/v8_embedding_test。



 作者简介


车雄生(johnche)

腾讯游戏开发工程师

腾讯游戏开发工程师,从事游戏开发工作多年,目前于腾讯游戏中台部门负责公共组件开发,是三个腾讯开源组件:xLua、InjectFix、Puerts的作者。


推荐阅读


程序员如何把你关注的内容推送到你眼前?揭秘信息流推荐背后的系统设计

在Exception的影响下,如何才能写出更高质量的C++代码?

自动的内存管理系统实操手册——Golang垃圾回收篇

自动的内存管理系统实操手册——Java和Golang对比篇

自动的内存管理系统实操手册——Java垃圾回收篇



浏览 92
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报