騷操作!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