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

导语 | 本文将介绍在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垃圾回收篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值