前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >c++服务器嵌入v8 js引擎胎教级教程

c++服务器嵌入v8 js引擎胎教级教程

作者头像
车雄生
发布2021-11-10 14:37:46
1K0
发布2021-11-10 14:37:46
举报
文章被收录于专栏:咩嗒咩嗒

逐渐有些原生语言项目(比如c++)因为希望有不停机更新的能力而引入脚本。这些团队往往有一套成熟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

直接上王道

代码语言:javascript
复制
//...各种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函数。

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文件。

Powered by Puerts

用个比较简单又有一定代表性的c++类为例:

代码语言:javascript
复制
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++里头做如下声明:

代码语言:javascript
复制
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类型定义):

代码语言:javascript
复制
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

这是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源码,进入源码目录,执行如下命令:

代码语言:javascript
复制
./configure --shared
make -j4

漫长的编译完成后,会在out/Release/下找到libnode.so.95文件,这就是动态库版本的node,接下来编译官方的嵌入式例子

代码语言:javascript
复制
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

跑一下试试

代码语言:javascript
复制
./a.out "console.log('hello world')"

跟着,我们把上一章节的TestClass,Puerts加入到这程序,然后在js里试试看?

代码语言:javascript
复制
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操作就可以运行:

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HelloWorld
  • Powered by Puerts
  • Powered by embedding Nodejs
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档