保姆級教程: c++游戲服務(wù)器嵌入v8 js引擎

導(dǎo)語 | 本文將介紹在c++游戲服務(wù)器上嵌入v8 js引擎的詳細(xì)教程,關(guān)鍵步驟都會附帶完整的可運(yùn)行代碼。并在文末為您附上github倉庫鏈接。
逐漸有些原生語言項(xiàng)目因?yàn)橄M胁煌C(jī)更新的能力而引入腳本。而且由于大多數(shù)項(xiàng)目已經(jīng)有現(xiàn)成的c++服務(wù)器框架,他們往往選擇把腳本作為庫嵌入到c++程序的做法。
服務(wù)器選用一個庫,最看重的莫過于穩(wěn)定性和性能了,在眾多腳本引擎中,v8這兩方面可謂佼佼者:穩(wěn)定性源自長時間各種方式的折騰,v8引擎每天那么多的實(shí)例跑在各種各樣的機(jī)器、環(huán)境下,跑著各種各樣的代碼,一天跑的代碼量比很多小眾的腳本引擎一輩子的代碼量還多,而且nodejs的應(yīng)用也驗(yàn)證了v8跑在服務(wù)器環(huán)境是沒問題的。
性能這塊,在jit的加持下,雖說比不上原生語言,但在腳本中肯定是第一檔的存在。
對于c++程序猿,v8還有個很誘人的地方:支持wasm,c++編譯成wasm在v8上跑,性能比js還能高一個臺階,而且還能熱更新。
v8引擎看上去很合適服務(wù)器使用,目前卻很少項(xiàng)目應(yīng)用到游戲服務(wù)器上,一些項(xiàng)目交流說有過這樣的想法,但不知道怎么做v8嵌入。于是有了本文,本文會循序漸進(jìn)的介紹怎么在linux c++程序里頭嵌入v8:
HelloWorld級別的示例;
c++類封裝到j(luò)s;
把v8改為嵌入式nodejs;
上述三步都會附帶完整的可運(yùn)行代碼,最后會附上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-----------------------------
}
以上一大堆代碼最終運(yùn)行效果只是打印了個“hello world”,沒接觸過的童靴是不是有點(diǎn)暈菜,別急,有我。
上述代碼我用分割線分成了6塊,其中:
第2塊是v8的啟動,第6塊是v8的關(guān)閉,除非你要定制啟動參數(shù),啟動多虛擬機(jī)啥的,否則這兩部分都是固定的;
第1塊有個Print函數(shù),和這函數(shù)同聲明的c++函數(shù),都可以注冊到j(luò)s環(huán)境里頭被js調(diào)用,函數(shù)只是簡單的把參數(shù)取出通過std::cout輸出;
第4塊把前面的Print函數(shù)注冊到j(luò)s的全局變量,名字也叫Print;
第5塊執(zhí)行了一段js代碼,調(diào)用了Print函數(shù)。
上述例子演示了怎么去啟動一個腳本,以及怎么從腳本調(diào)用原生。在Print只是簡單的取一個參數(shù)進(jìn)行打印,如果有更多個數(shù)及種類的參數(shù)呢?更復(fù)雜的是一個c++類有構(gòu)造函數(shù),成員變量,有成員函數(shù),靜態(tài)函數(shù),還有繼承,重載等等,c++類如果需要封裝不是十分麻煩?
這就輪到puerts出場了,為服務(wù)器童鞋科普下:puerts最初是Unreal Engine、Unity游戲引擎下的typescript編程解決方案,但游戲引擎以外的環(huán)境也逐步在支持,其中任意C#環(huán)境早已支持,而c++ 11以上環(huán)境,最近也加入支持之列。通過puerts,我們僅僅只需對c++進(jìn)行些聲明操作,即可被js使用,甚至可以根據(jù)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));
當(dāng)然,要支持這些,還需要對puerts做一定的初始化操作,在這就不再贅述,各位可于文后鏈接獲取代碼,對比第一版Helloworld即可得知用法。
至此,我們能在c++程序里執(zhí)行js代碼, js能調(diào)用到c++代碼。這對一些項(xiàng)目已經(jīng)足夠了。
不過我們嵌入的v8引擎,只實(shí)現(xiàn)了es規(guī)范語法以及api,像setTimeout這種耳熟能詳?shù)腶pi,都不是es規(guī)范的內(nèi)容,其次有的項(xiàng)目組希望能對接npm上豐富的組件,那有沒可能往c++程序嵌入一個nodejs呢?請看下一章節(jié)。
三、Powered by embedding Nodejs
第一步我們要編譯libnode.so,下載或者clone node源碼,進(jìn)入源碼目錄,執(zhí)行如下命令:
./configure --shared
make -j4
漫長的編譯完成后,會在out/Release/下找到libnode.so.95文件,這就是動態(tài)庫版本的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')"
跟著,我們把上一章節(jié)的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++類調(diào)用之外,還加了nodejs api的調(diào)用,以證明這確實(shí)是個完整的nodejs環(huán)境。
nodejs的嵌入可能要了解的情況更多,它內(nèi)部有一套事件循環(huán)處理邏輯,也會啟動些線程,要注意這些是否和原來的服務(wù)器框架有沖突。相比之下,上一章節(jié)的純v8環(huán)境只是一個庫,它跑不跑取決于你是否調(diào)用,會簡單得多。
附上完整的實(shí)例代碼以及編譯配置,按readme操作就可以運(yùn)行:
https://github.com/chexiongsheng/v8_embedding_test。
作者簡介
車雄生(johnche)
騰訊游戲開發(fā)工程師
騰訊游戲開發(fā)工程師,從事游戲開發(fā)工作多年,目前于騰訊游戲中臺部門負(fù)責(zé)公共組件開發(fā),是三個騰訊開源組件:xLua、InjectFix、Puerts的作者。
推薦閱讀
程序員如何把你關(guān)注的內(nèi)容推送到你眼前?揭秘信息流推薦背后的系統(tǒng)設(shè)計(jì)
在Exception的影響下,如何才能寫出更高質(zhì)量的C++代碼?
自動的內(nèi)存管理系統(tǒng)實(shí)操手冊——Golang垃圾回收篇
自動的內(nèi)存管理系統(tǒng)實(shí)操手冊——Java和Golang對比篇
自動的內(nèi)存管理系統(tǒng)實(shí)操手冊——Java垃圾回收篇


