electron/nodejs實現(xiàn)調(diào)用golang函數(shù)

nodejs 調(diào)用golang

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

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

思路

golang 支持編譯成c shared library, 也就是系統(tǒng)中常見的.so(windows下是dll)后綴的動態(tài)鏈接庫文件. c++可以調(diào)用動態(tài)鏈接庫,所以基本思路是golang開發(fā)主要功能, c++開發(fā)插件包裝golang函數(shù),實現(xiàn)中轉(zhuǎn)調(diào)用

對于類型問題, 為了方便處理, 暴露的golang函數(shù)統(tǒng)一接受并返回字符串, 需要傳的參數(shù)都經(jīng)過json編碼, 返回值亦然. 這里實現(xiàn)了3種調(diào)用方式, 同步調(diào)用,異步調(diào)用和帶進度回調(diào)的的異步調(diào)用.應該能滿足大部分需求

參考

golang cgo支持: https://golang.org/cmd/cgo/

實現(xiàn)

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

golang部分

// gofun.go
package main

// int a;
// typedef void (*cb)(char* data);
// extern void callCb(cb callback, char* extra, char* arg);
import "C" // C是一個虛包, 上面的注釋是c代碼, 可以在golang中加 `C.` 前綴訪問, 具體參考上面給出的文檔
import "time"

//export hello
func hello(arg *C.char) *C.char  {
    //name := gjson.Get(arg, "name")
    //return "hello" + name.String()
    return C.CString("hello peter:::" + C.GoString(arg))
} // 通過export注解,把這個函數(shù)暴露到動態(tài)鏈接庫里面

//export helloP
func helloP(arg *C.char, cb C.cb, extra *C.char) *C.char  {
    C.callCb(cb, extra, C.CString("one"))
    time.Sleep(time.Second)
    C.callCb(cb, extra, C.CString("two"))
    return C.CString("hello peter:::" + C.GoString(arg))
}

func main() {
    println("go main func")
}

// bridge.go
package main

// typedef void (*cb)(char* extra, char* data);
// void callCb(cb callback, char* extra , char* arg) { // c的回調(diào), go將通過這個函數(shù)回調(diào)c代碼
//    callback(extra,arg);
// }
import "C"

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

c++部分

// ext.cpp
#include <node.h>
#include <uv.h>

#include <dlfcn.h>
#include <cstring>

#include <map>

#include "go/gofun.h"
#include <stdio.h>

using namespace std;

using namespace node;
using namespace v8;

// 調(diào)用go的線程所需要的結構體, 把相關數(shù)據(jù)都封裝進去, 同步調(diào)用不需要用到這個
struct GoThreadData {
    char func[128]{}; // 調(diào)用的go函數(shù)名稱
    char* arg{}; // 傳給go的參數(shù), json編碼
    char* result{}; // go返回值
    bool hasError = false; // 是否有錯誤
    const char *error{}; // 錯誤信息
    char* progress{}; // 進度回調(diào)所需要傳的進度值
    bool isProgress = false; // 是否是進度調(diào)用, 用來區(qū)分普通調(diào)用
    Persistent<Function, CopyablePersistentTraits<Function>> onProgress{}; // js的進度回調(diào)
    Persistent<Function, CopyablePersistentTraits<Function>> callback{}; // js 返回值回調(diào)
    Persistent<Function, CopyablePersistentTraits<Function>> onError{}; // js的出錯回調(diào)
    Isolate* isolate{}; // js引擎實例
    uv_async_t* progressReq;// 由于調(diào)用go異步函數(shù)會新開啟一個進程, 所以go函數(shù)不在主進程被調(diào)用, 但是v8規(guī)定,調(diào)用js的函數(shù)必須在住線程當中進行,否則報錯, 所以這里用到了libuv的接口, 用來在子線程中通知主線程執(zhí)行回調(diào).
};


// 下面的函數(shù)會在主線程中執(zhí)行, 由libuv庫進行調(diào)用, 這里用來處理go回調(diào)過來進度值
void progressCallbackFunc(uv_async_t *handle) {
    HandleScope handle_scope(Isolate::GetCurrent());
    GoThreadData*  goThreadData = (GoThreadData *) handle->data;
    // printf("%s___%d__%s\n", __FUNCTION__, (int)uv_thread_self() , goThreadData->progress);
    Local<Value> argv[1] = {String::NewFromUtf8(goThreadData->isolate, goThreadData->progress)};
    Local<Function>::New(goThreadData->isolate, goThreadData->onProgress)->Call(goThreadData->isolate->GetCurrentContext()->Global(), 1, argv); // 從goThreadData獲取進度值并回調(diào)給js
}

// uv異步句柄關閉回調(diào)
void close_cb(uv_handle_t* handle)
{
    // printf("close the async handle!\n");
}

// 這個函數(shù)傳給golang調(diào)用, 當golang通知js有進度更新時這里會執(zhí)行,extra參數(shù)是一個GoThreadData, 用來區(qū)分是那一次調(diào)用的回調(diào), 可以將GoThreadData理解為go函數(shù)調(diào)用上下文
void goCallback(char * extra, char * arg) {
    // printf("%s: %d\n", __FUNCTION__,  (int)uv_thread_self());
    GoThreadData* data = (GoThreadData *) extra;
    delete data->progress;
    data->progress = arg; // 把進度信息放到上下文當中
    // printf("%d:%s---%s----%s\n",__LINE__, arg, data->func, data->progress);
    uv_async_send(data->progressReq); // 通知主線程, 這里會導致上面的progressCallbackFunc執(zhí)行
}

void * goLib = nullptr; // 打開的gofun.so的句柄

typedef char* (*GoFunc)(char* p0); // go同步函數(shù)和不帶進度的異步函數(shù)
typedef char* (*GoFuncWithProgress)(char* p0, void (*goCallback) (char* extra, char * arg), char * data); // go帶進度回調(diào)的異步函數(shù)

map<string, GoFunc> loadedGoFunc; // 一個map用來存儲已經(jīng)加載啦那些函數(shù)
map<string, GoFuncWithProgress> loadedGoFuncWithProgress; // 和上面類似

// 加載 go 拓展, 暴露給js 通過路徑加載so文件
void loadGo(const FunctionCallbackInfo<Value>& args) {
    String::Utf8Value path(args[0]->ToString());
    Isolate* isolate = args.GetIsolate();
    void *handle = dlopen(*path, RTLD_LAZY);
    if (!handle) {
        isolate->ThrowException(Exception::Error(
                String::NewFromUtf8(isolate, "拓展加載失敗, 請檢查路徑和權限")
        ));
        return;
    }
    if (goLib) dlclose(goLib);
    goLib = handle; // 保存到全局變量當中
    loadedGoFunc.empty(); // 覆蓋函數(shù)
    args.GetReturnValue().Set(true); // 返回true給js
}

// 釋放go函數(shù)調(diào)用上下文結構體的內(nèi)存
void freeGoThreadData(GoThreadData* data) {
    delete data->result;
    delete data->progress;
    delete data->arg;
    delete data->error;
    delete data;
}

// 由libuv在主線程中進行調(diào)用, 當go函數(shù)返回時,這里會被調(diào)用
void afterGoTread (uv_work_t* req, int status) {
    // printf("%s: %d\n", __FUNCTION__,  (int)uv_thread_self());
    auto * goThreadData = (GoThreadData*) req->data;
    HandleScope handle_scope(Isolate::GetCurrent());// 這里是必須的,調(diào)用js函數(shù)需要一個handle scope
    if (goThreadData->hasError) { // 如果有錯誤, 生成一個錯誤實例并傳給js錯誤回調(diào)
        Local<Value> argv[1] = {Exception::Error(
                String::NewFromUtf8(goThreadData->isolate, goThreadData->error)
        )};

        Local<Function>::New(goThreadData->isolate, goThreadData->onError)->Call(goThreadData->isolate->GetCurrentContext()->Global(), 1, argv);
        return;
    }
    // 沒有錯誤, 把結果回調(diào)給js
    Local<Value> argv[1] = {String::NewFromUtf8(goThreadData->isolate, goThreadData->result)};
    Local<Function>::New(goThreadData->isolate, goThreadData->callback)->Call(goThreadData->isolate->GetCurrentContext()->Global(), 1, argv);
    if (goThreadData->isProgress) {
        // printf(((GoThreadData *)goThreadData->progressReq->data)->result);
        uv_close((uv_handle_t*) goThreadData->progressReq, close_cb); // 這里需要把通知js進度的事件刪除, 不然這個事件會一直存在時間循環(huán)中, node進程也不會退出
    }
    // 釋放內(nèi)存
    freeGoThreadData(goThreadData);
}




// 工作線程, 在這個函數(shù)中調(diào)用go
void callGoThread(uv_work_t* req)
{
    // 從uv_work_t的結構體中獲取我們定義的入?yún)⒔Y構
    auto * goThreadData = (GoThreadData*) req->data;

    // printf("%s: %d\n", __FUNCTION__,  (int)uv_thread_self());
    // 檢查內(nèi)核是否加載
    if (!goLib) {
        goThreadData->hasError = true;
        String::NewFromUtf8(goThreadData->isolate, "請先加載內(nèi)核");
        goThreadData->error = "請先加載內(nèi)核";
        return;
    }

    if (!goThreadData->isProgress) {
        // 檢查函數(shù)是否加載
        if (! loadedGoFunc[goThreadData->func]) {
            auto goFunc = (GoFunc) dlsym(goLib, goThreadData->func);
            if(!goFunc)
            {
                goThreadData->hasError = true;
                goThreadData->error = "函數(shù)加載失敗";
                return;
            }
            // printf("loaded %s\n", goThreadData->func);
            loadedGoFunc[goThreadData->func] = goFunc;
        }

        // 調(diào)用go函數(shù)
        GoFunc func = loadedGoFunc[goThreadData->func];
        char * result = func(goThreadData->arg);
        // printf("%d:%s\n-----------------------------\n", __LINE__, result);
        // printf("%d:%s\n-----------------------------\n", __LINE__, goThreadData->arg);
        goThreadData->result = result;
        return;
    }

    // 有progress回調(diào)函數(shù)的
    // 檢查函數(shù)是否加載
    if (! loadedGoFuncWithProgress[goThreadData->func]) {
        auto goFunc = (GoFuncWithProgress) dlsym(goLib, goThreadData->func);
        if(!goFunc)
        {
            goThreadData->hasError = true;
            goThreadData->error = "函數(shù)加載失敗";
            return;
        }
        // printf("loaded %s\n", goThreadData->func);
        loadedGoFuncWithProgress[goThreadData->func] = goFunc;
    }

    // 調(diào)用go函數(shù)
    GoFuncWithProgress func = loadedGoFuncWithProgress[goThreadData->func];
    char * result = func(goThreadData->arg, goCallback, (char*) goThreadData);
    // printf("%d:%s\n-----------------------------\n", __LINE__, result);
    // printf("%d:%s\n-----------------------------\n", __LINE__, goThreadData->arg);
    goThreadData->result = result;
}


// 暴露給js的,用來調(diào)用go的非同步函數(shù)(同步只是相對js而言, 實際上go函數(shù)還是同步執(zhí)行的)
void callGoAsync(const FunctionCallbackInfo<Value>& args) {
    // printf("%s: %d\n", __FUNCTION__,  (int)uv_thread_self());

    Isolate* isolate = args.GetIsolate();

    // 檢查傳入的參數(shù)的個數(shù)
    if (args.Length() < 3 || (
            !args[0]->IsString()
            || !args[1]->IsString()
            || !args[2]->IsFunction()
            || !args[3]->IsFunction()
    )) {
        // 拋出一個錯誤并傳回到 JavaScript
        isolate->ThrowException(Exception::TypeError(
                String::NewFromUtf8(isolate, "調(diào)用格式: 函數(shù)名稱, JSON參數(shù), 成功回調(diào), 錯誤回調(diào)")));
        return;
    }
    // 參數(shù)格式化, 構造線程數(shù)據(jù)
    auto goThreadData = new GoThreadData;

   // 有第5個參數(shù), 說明是調(diào)用有進度回調(diào)的go函數(shù)
    if (args.Length() >= 5) {
        if (!args[4]->IsFunction()) {
            isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "如果有第5個參數(shù), 請傳入Progress回調(diào)")));
            return;
        } else {
            goThreadData->isProgress = true;
            goThreadData->onProgress.Reset(isolate, Local<Function>::Cast(args[4]));
        }
    }

    // go調(diào)用上下文的初始化
    goThreadData->callback.Reset(isolate, Local<Function>::Cast(args[2]));

    goThreadData->onError.Reset(isolate, Local<Function>::Cast(args[3]));
    goThreadData->isolate = isolate;
    v8::String::Utf8Value arg(args[1]->ToString());
    goThreadData->arg = (char*)(new string(*arg))->data();
    v8::String::Utf8Value func(args[0]->ToString());
    strcpy(goThreadData->func, *func);

    // 調(diào)用libuv實現(xiàn)多線程
    auto req = new uv_work_t();
    req->data = goThreadData;

    // 如果是有進度回調(diào)的需要注冊一個異步事件, 以便在子線程回調(diào)js
    if (goThreadData->isProgress) {
        goThreadData->progressReq = new uv_async_t();
        goThreadData->progressReq->data = (void *) goThreadData;
        uv_async_init(uv_default_loop(), goThreadData->progressReq, progressCallbackFunc);
    }

    // 調(diào)用libuv的線程處理函數(shù)
    uv_queue_work(uv_default_loop(), req, callGoThread, afterGoTread);

}


// 模塊初始化, 注冊暴露給js的函數(shù)
void init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "loadCore", loadGo);
    NODE_SET_METHOD(exports, "callCoreAsync", callGoAsync);
}

NODE_MODULE(addon, init)

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

{
    "targets": [
        {
            "target_name": "addon",
            "sources": [ "ext.cpp" ]
        }
    ]
}

測試的js代碼

// test.js
let addon = require('./build/Release/addon');
let success = function (data) {
    console.log("leo")
    console.log(data);
}
let fail = function (error) {
    console.log('peter')
    console.log(error)
}
addon.loadCore('./go/gofun.1.so')
addon.callCoreAsync('hello', JSON.stringify({name: '我愛你'}), success, fail)
setTimeout(function () {
    addon.callCoreAsync('helloP', JSON.stringify({name: '我愛你1'}), success, fail, function (data) {
        console.log('js log:' + data)
    })
})

輸出如下:


2018-05-14 14-58-35 的屏幕截圖.png

踩了不少坑, 主要是網(wǎng)上對于node addon開發(fā)的相關文章都過時了, 自己摸索著終于搞完了, 特此分享一下.

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子懈叹,更是在濱河造成了極大的恐慌勾笆,老刑警劉巖芳肌,帶你破解...
    沈念sama閱讀 210,835評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件困食,死亡現(xiàn)場離奇詭異碗脊,居然都是意外死亡啼肩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評論 2 383
  • 文/潘曉璐 我一進店門衙伶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來祈坠,“玉大人,你說我怎么就攤上這事矢劲∩饩校” “怎么了?”我有些...
    開封第一講書人閱讀 156,481評論 0 345
  • 文/不壞的土叔 我叫張陵卧须,是天一觀的道長另绩。 經(jīng)常有香客問我儒陨,道長花嘶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,303評論 1 282
  • 正文 為了忘掉前任蹦漠,我火速辦了婚禮椭员,結果婚禮上,老公的妹妹穿的比我還像新娘笛园。我一直安慰自己隘击,他們只是感情好,可當我...
    茶點故事閱讀 65,375評論 5 384
  • 文/花漫 我一把揭開白布研铆。 她就那樣靜靜地躺著埋同,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棵红。 梳的紋絲不亂的頭發(fā)上凶赁,一...
    開封第一講書人閱讀 49,729評論 1 289
  • 那天,我揣著相機與錄音逆甜,去河邊找鬼虱肄。 笑死,一個胖子當著我的面吹牛交煞,可吹牛的內(nèi)容都是我干的咏窿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,877評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼素征,長吁一口氣:“原來是場噩夢啊……” “哼集嵌!你這毒婦竟也來了萝挤?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,633評論 0 266
  • 序言:老撾萬榮一對情侶失蹤纸淮,失蹤者是張志新(化名)和其女友劉穎平斩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咽块,經(jīng)...
    沈念sama閱讀 44,088評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡绘面,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,443評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了侈沪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揭璃。...
    茶點故事閱讀 38,563評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖亭罪,靈堂內(nèi)的尸體忽然破棺而出瘦馍,到底是詐尸還是另有隱情,我是刑警寧澤应役,帶...
    沈念sama閱讀 34,251評論 4 328
  • 正文 年R本政府宣布情组,位于F島的核電站,受9級特大地震影響箩祥,放射性物質(zhì)發(fā)生泄漏院崇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,827評論 3 312
  • 文/蒙蒙 一袍祖、第九天 我趴在偏房一處隱蔽的房頂上張望底瓣。 院中可真熱鬧,春花似錦蕉陋、人聲如沸捐凭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茁肠。三九已至,卻和暖如春缩举,著一層夾襖步出監(jiān)牢的瞬間垦梆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評論 1 264
  • 我被黑心中介騙來泰國打工蚁孔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留奶赔,地道東北人。 一個月前我還...
    沈念sama閱讀 46,240評論 2 360
  • 正文 我出身青樓杠氢,卻偏偏與公主長得像站刑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鼻百,可洞房花燭夜當晚...
    茶點故事閱讀 43,435評論 2 348

推薦閱讀更多精彩內(nèi)容