騷操作!electron-nodejs 實現調用 go 函數

【導讀】騷操作!nodejs 開發桌面應用、如何實現和 go 交互?全乾工程師詳細解讀 go 被其他語言調用的編譯流程。

nodejs 調用 golang

最近在用 electron 開發一個 gui 程序, 有一些代碼不能暴露出來. 但是 JavaScript 是解釋型語言, 混淆什麼的都難以保證代碼不被泄露. 所以就想寫一個原生插件, 暴露接口 js 調用. js 原生插件通常用 C++ 開發, 但是本人不會, 去學習的話涉及到的知識太多了, 短期內肯定是不行的, 不過最近正好在學 golang, 就想着能不能用 golang 去寫.

本人目前是 php 程序猿, c++ 和 golang 都是入門水平, 若是代碼中有任何不妥, 還望大神不吝賜教. 見笑了.

思路

golang 支持編譯成 c shared library, 也就是系統中常見的. so(windows 下是 dll) 後綴的動態鏈接庫文件. c++ 可以調用動態鏈接庫, 所以基本思路是 golang 開發主要功能, c++ 開發插件包裝 golang 函數, 實現中轉調用

對於類型問題, 爲了方便處理, 暴露的 golang 函數統一接受並返回字符串, 需要傳的參數都經過 json 編碼, 返回值亦然. 這裏實現了 3 種調用方式, 同步調用, 異步調用和帶進度回調的的異步調用. 應該能滿足大部分需求

實現

不多說直接上代碼, 相關說明都寫到註釋中了

golang 部分

 1// gofun.go
 2package main
 3
 4// int a;
 5// typedef void (*cb)(char* data);
 6// extern void callCb(cb callback, char* extra, char* arg);
 7import "C" // C是一個虛包, 上面的註釋是c代碼, 可以在golang中加 `C.` 前綴訪問, 具體參考上面給出的文檔
 8import "time"
 9
10//export hello
11func hello(arg *C.char) *C.char  {
12    //name := gjson.Get(arg, "name")
13    //return "hello" + name.String()
14    return C.CString("hello peter:::" + C.GoString(arg))
15} // 通過export註解,把這個函數暴露到動態鏈接庫裏面
16
17//export helloP
18func helloP(arg *C.char, cb C.cb, extra *C.char) *C.char  {
19    C.callCb(cb, extra, C.CString("one"))
20    time.Sleep(time.Second)
21    C.callCb(cb, extra, C.CString("two"))
22    return C.CString("hello peter:::" + C.GoString(arg))
23}
24
25func main() {
26    println("go main func")
27}
28
29
 1// bridge.go
 2package main
 3
 4// typedef void (*cb)(char* extra, char* data);
 5// void callCb(cb callback, char* extra , char* arg) { // c的回調, go將通過這個函數回調c代碼
 6//    callback(extra,arg);
 7// }
 8import "C"
 9
10
11

通過命令go build \-o gofun.so \-buildmode=c-shared gofun.go bridge.go 編譯得到 gofun.so 的鏈接庫文件
通過 go tool cgo \-- \-exportheader gofun.go 可以得到 gofun.h 頭文件, 可以方便在 c++ 中使用

c++ 部分

  1// ext.cpp
  2#include <node.h>
  3#include <uv.h>
  4
  5#include <dlfcn.h>
  6#include <cstring>
  7
  8#include <map>
  9
 10#include "go/gofun.h"
 11#include <stdio.h>
 12
 13using namespace std;
 14
 15using namespace node;
 16using namespace v8;
 17
 18// 調用go的線程所需要的結構體, 把相關數據都封裝進去, 同步調用不需要用到這個
 19struct GoThreadData {
 20    char func[128]{}; // 調用的go函數名稱
 21    char* arg{}; // 傳給go的參數, json編碼
 22    char* result{}; // go返回值
 23    bool hasError = false; // 是否有錯誤
 24    const char *error{}; // 錯誤信息
 25    char* progress{}; // 進度回調所需要傳的進度值
 26    bool isProgress = false; // 是否是進度調用, 用來區分普通調用
 27    Persistent<Function, CopyablePersistentTraits<Function>> onProgress{}; // js的進度回調
 28    Persistent<Function, CopyablePersistentTraits<Function>> callback{}; // js 返回值回調
 29    Persistent<Function, CopyablePersistentTraits<Function>> onError{}; // js的出錯回調
 30    Isolate* isolate{}; // js引擎實例
 31    uv_async_t* progressReq;// 由於調用go異步函數會新開啓一個進程, 所以go函數不在主進程被調用, 但是v8規定,調用js的函數必須在住線程當中進行,否則報錯, 所以這裏用到了libuv的接口, 用來在子線程中通知主線程執行回調.
 32};
 33
 34
 35// 下面的函數會在主線程中執行, 由libuv庫進行調用, 這裏用來處理go回調過來進度值
 36void progressCallbackFunc(uv_async_t *handle) {
 37    HandleScope handle_scope(Isolate::GetCurrent());
 38    GoThreadData*  goThreadData = (GoThreadData *) handle->data;
 39    // printf("%s___%d__%s\n", __FUNCTION__, (int)uv_thread_self() , goThreadData->progress);
 40    Local<Value> argv[1] = {String::NewFromUtf8(goThreadData->isolate, goThreadData->progress)};
 41    Local<Function>::New(goThreadData->isolate, goThreadData->onProgress)->Call(goThreadData->isolate->GetCurrentContext()->Global(), 1, argv); // 從goThreadData獲取進度值並回調給js
 42}
 43
 44// uv異步句柄關閉回調
 45void close_cb(uv_handle_t* handle)
 46{
 47    // printf("close the async handle!\n");
 48}
 49
 50// 這個函數傳給golang調用, 當golang通知js有進度更新時這裏會執行,extra參數是一個GoThreadData, 用來區分是那一次調用的回調, 可以將GoThreadData理解爲go函數調用上下文
 51void goCallback(char * extra, char * arg) {
 52    // printf("%s: %d\n", __FUNCTION__,  (int)uv_thread_self());
 53    GoThreadData* data = (GoThreadData *) extra;
 54    delete data->progress;
 55    data->progress = arg; // 把進度信息放到上下文當中
 56    // printf("%d:%s---%s----%s\n",__LINE__, arg, data->func, data->progress);
 57    uv_async_send(data->progressReq); // 通知主線程, 這裏會導致上面的progressCallbackFunc執行
 58}
 59
 60void * goLib = nullptr; // 打開的gofun.so的句柄
 61
 62typedef char* (*GoFunc)(char* p0); // go同步函數和不帶進度的異步函數
 63typedef char* (*GoFuncWithProgress)(char* p0, void (*goCallback) (char* extra, char * arg), char * data); // go帶進度回調的異步函數
 64
 65map<string, GoFunc> loadedGoFunc; // 一個map用來存儲已經加載啦那些函數
 66map<string, GoFuncWithProgress> loadedGoFuncWithProgress; // 和上面類似
 67
 68// 加載 go 拓展, 暴露給js 通過路徑加載so文件
 69void loadGo(const FunctionCallbackInfo<Value>& args) {
 70    String::Utf8Value path(args[0]->ToString());
 71    Isolate* isolate = args.GetIsolate();
 72    void *handle = dlopen(*path, RTLD_LAZY);
 73    if (!handle) {
 74        isolate->ThrowException(Exception::Error(
 75                String::NewFromUtf8(isolate, "拓展加載失敗, 請檢查路徑和權限")
 76        ));
 77        return;
 78    }
 79    if (goLib) dlclose(goLib);
 80    goLib = handle; // 保存到全局變量當中
 81    loadedGoFunc.empty(); // 覆蓋函數
 82    args.GetReturnValue().Set(true); // 返回true給js
 83}
 84
 85// 釋放go函數調用上下文結構體的內存
 86void freeGoThreadData(GoThreadData* data) {
 87    delete data->result;
 88    delete data->progress;
 89    delete data->arg;
 90    delete data->error;
 91    delete data;
 92}
 93
 94// 由libuv在主線程中進行調用, 當go函數返回時,這裏會被調用
 95void afterGoTread (uv_work_t* req, int status) {
 96    // printf("%s: %d\n", __FUNCTION__,  (int)uv_thread_self());
 97    auto * goThreadData = (GoThreadData*) req->data;
 98    HandleScope handle_scope(Isolate::GetCurrent());// 這裏是必須的,調用js函數需要一個handle scope
 99    if (goThreadData->hasError) { // 如果有錯誤, 生成一個錯誤實例並傳給js錯誤回調
100        Local<Value> argv[1] = {Exception::Error(
101                String::NewFromUtf8(goThreadData->isolate, goThreadData->error)
102        )};
103
104        Local<Function>::New(goThreadData->isolate, goThreadData->onError)->Call(goThreadData->isolate->GetCurrentContext()->Global(), 1, argv);
105        return;
106    }
107    // 沒有錯誤, 把結果回調給js
108    Local<Value> argv[1] = {String::NewFromUtf8(goThreadData->isolate, goThreadData->result)};
109    Local<Function>::New(goThreadData->isolate, goThreadData->callback)->Call(goThreadData->isolate->GetCurrentContext()->Global(), 1, argv);
110    if (goThreadData->isProgress) {
111        // printf(((GoThreadData *)goThreadData->progressReq->data)->result);
112        uv_close((uv_handle_t*) goThreadData->progressReq, close_cb); // 這裏需要把通知js進度的事件刪除, 不然這個事件會一直存在時間循環中, node進程也不會退出
113    }
114    // 釋放內存
115    freeGoThreadData(goThreadData);
116}
117
118
119
120
121// 工作線程, 在這個函數中調用go
122void callGoThread(uv_work_t* req)
123{
124    // 從uv_work_t的結構體中獲取我們定義的入參結構
125    auto * goThreadData = (GoThreadData*) req->data;
126
127    // printf("%s: %d\n", __FUNCTION__,  (int)uv_thread_self());
128    // 檢查內核是否加載
129    if (!goLib) {
130        goThreadData->hasError = true;
131        String::NewFromUtf8(goThreadData->isolate, "請先加載內核");
132        goThreadData->error = "請先加載內核";
133        return;
134    }
135
136    if (!goThreadData->isProgress) {
137        // 檢查函數是否加載
138        if (! loadedGoFunc[goThreadData->func]) {
139            auto goFunc = (GoFunc) dlsym(goLib, goThreadData->func);
140            if(!goFunc)
141            {
142                goThreadData->hasError = true;
143                goThreadData->error = "函數加載失敗";
144                return;
145            }
146            // printf("loaded %s\n", goThreadData->func);
147            loadedGoFunc[goThreadData->func] = goFunc;
148        }
149
150        // 調用go函數
151        GoFunc func = loadedGoFunc[goThreadData->func];
152        char * result = func(goThreadData->arg);
153        // printf("%d:%s\n-----------------------------\n", __LINE__, result);
154        // printf("%d:%s\n-----------------------------\n", __LINE__, goThreadData->arg);
155        goThreadData->result = result;
156        return;
157    }
158
159    // 有progress回調函數的
160    // 檢查函數是否加載
161    if (! loadedGoFuncWithProgress[goThreadData->func]) {
162        auto goFunc = (GoFuncWithProgress) dlsym(goLib, goThreadData->func);
163        if(!goFunc)
164        {
165            goThreadData->hasError = true;
166            goThreadData->error = "函數加載失敗";
167            return;
168        }
169        // printf("loaded %s\n", goThreadData->func);
170        loadedGoFuncWithProgress[goThreadData->func] = goFunc;
171    }
172
173    // 調用go函數
174    GoFuncWithProgress func = loadedGoFuncWithProgress[goThreadData->func];
175    char * result = func(goThreadData->arg, goCallback, (char*) goThreadData);
176    // printf("%d:%s\n-----------------------------\n", __LINE__, result);
177    // printf("%d:%s\n-----------------------------\n", __LINE__, goThreadData->arg);
178    goThreadData->result = result;
179}
180
181
182// 暴露給js的,用來調用go的非同步函數(同步只是相對js而言, 實際上go函數還是同步執行的)
183void callGoAsync(const FunctionCallbackInfo<Value>& args) {
184    // printf("%s: %d\n", __FUNCTION__,  (int)uv_thread_self());
185
186    Isolate* isolate = args.GetIsolate();
187
188    // 檢查傳入的參數的個數
189    if (args.Length() < 3 || (
190            !args[0]->IsString()
191            || !args[1]->IsString()
192            || !args[2]->IsFunction()
193            || !args[3]->IsFunction()
194    )) {
195        // 拋出一個錯誤並傳回到 JavaScript
196        isolate->ThrowException(Exception::TypeError(
197                String::NewFromUtf8(isolate, "調用格式: 函數名稱, JSON參數, 成功回調, 錯誤回調")));
198        return;
199    }
200    // 參數格式化, 構造線程數據
201    auto goThreadData = new GoThreadData;
202
203   // 有第5個參數, 說明是調用有進度回調的go函數
204    if (args.Length() >= 5) {
205        if (!args[4]->IsFunction()) {
206            isolate->ThrowException(Exception::TypeError(
207                    String::NewFromUtf8(isolate, "如果有第5個參數, 請傳入Progress回調")));
208            return;
209        } else {
210            goThreadData->isProgress = true;
211            goThreadData->onProgress.Reset(isolate, Local<Function>::Cast(args[4]));
212        }
213    }
214
215    // go調用上下文的初始化
216    goThreadData->callback.Reset(isolate, Local<Function>::Cast(args[2]));
217
218    goThreadData->onError.Reset(isolate, Local<Function>::Cast(args[3]));
219    goThreadData->isolate = isolate;
220    v8::String::Utf8Value arg(args[1]->ToString());
221    goThreadData->arg = (char*)(new string(*arg))->data();
222    v8::String::Utf8Value func(args[0]->ToString());
223    strcpy(goThreadData->func, *func);
224
225    // 調用libuv實現多線程
226    auto req = new uv_work_t();
227    req->data = goThreadData;
228
229    // 如果是有進度回調的需要註冊一個異步事件, 以便在子線程回調js
230    if (goThreadData->isProgress) {
231        goThreadData->progressReq = new uv_async_t();
232        goThreadData->progressReq->data = (void *) goThreadData;
233        uv_async_init(uv_default_loop(), goThreadData->progressReq, progressCallbackFunc);
234    }
235
236    // 調用libuv的線程處理函數
237    uv_queue_work(uv_default_loop(), req, callGoThread, afterGoTread);
238
239}
240
241
242// 模塊初始化, 註冊暴露給js的函數
243void init(Local<Object> exports) {
244    NODE_SET_METHOD(exports, "loadCore", loadGo);
245    NODE_SET_METHOD(exports, "callCoreAsync", callGoAsync);
246}
247
248NODE_MODULE(addon, init)
249
250

通過 node-gyp build 編譯出 addon.node 原生模塊文件, 下附配置文件, 請參考 nodejs 官方文檔

 1{
 2    "targets": [
 3        {
 4            "target_name": "addon",
 5            "sources": [ "ext.cpp" ]
 6        }
 7    ]
 8}
 9
10

測試的 js 代碼

 1// test.js
 2let addon = require('./build/Release/addon');
 3let success = function (data) {
 4    console.log("leo")
 5    console.log(data);
 6}
 7let fail = function (error) {
 8    console.log('peter')
 9    console.log(error)
10}
11addon.loadCore('./go/gofun.1.so')
12addon.callCoreAsync('hello', JSON.stringify({name: '我愛你'}), success, fail)
13setTimeout(function () {
14    addon.callCoreAsync('helloP', JSON.stringify({name: '我愛你1'}), success, fail, function (data) {
15        console.log('js log:' + data)
16    })
17})
18
19
20

輸出如下:

踩了不少坑, 主要是網上對於 node addon 開發的相關文章都過時了, 自己摸索着終於搞完了, 特此分享一下.

轉自:PeterQ1998

鏈接:www.jianshu.com/p/a3be0d206d4c

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IrMp_PR8LkxvJH-I2dy91A