zoukankan      html  css  js  c++  java
  • electron/nodejs实现调用golang函数

    https://www.jianshu.com/p/a3be0d206d4c

    思路

    golang 支持编译成c shared library, 也就是系统中常见的.so(windows下是dll)后缀的动态链接库文件. c++可以调用动态链接库,所以基本思路是golang开发主要功能, c++开发插件包装golang函数,实现中转调用

    对于类型问题, 为了方便处理, 暴露的golang函数统一接受并返回字符串, 需要传的参数都经过json编码, 返回值亦然. 这里实现了3种调用方式, 同步调用,异步调用和带进度回调的的异步调用.应该能满足大部分需求

    参考

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

    实现

    不多说直接上代码, 相关说明都写到注释中了

    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注解,把这个函数暴露到动态链接库里面
    
    //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的回调, go将通过这个函数回调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;
    
    // 调用go的线程所需要的结构体, 把相关数据都封装进去, 同步调用不需要用到这个
    struct GoThreadData {
        char func[128]{}; // 调用的go函数名称
        char* arg{}; // 传给go的参数, json编码
        char* result{}; // go返回值
        bool hasError = false; // 是否有错误
        const char *error{}; // 错误信息
        char* progress{}; // 进度回调所需要传的进度值
        bool isProgress = false; // 是否是进度调用, 用来区分普通调用
        Persistent<Function, CopyablePersistentTraits<Function>> onProgress{}; // js的进度回调
        Persistent<Function, CopyablePersistentTraits<Function>> callback{}; // js 返回值回调
        Persistent<Function, CopyablePersistentTraits<Function>> onError{}; // js的出错回调
        Isolate* isolate{}; // js引擎实例
        uv_async_t* progressReq;// 由于调用go异步函数会新开启一个进程, 所以go函数不在主进程被调用, 但是v8规定,调用js的函数必须在住线程当中进行,否则报错, 所以这里用到了libuv的接口, 用来在子线程中通知主线程执行回调.
    };
    
    
    // 下面的函数会在主线程中执行, 由libuv库进行调用, 这里用来处理go回调过来进度值
    void progressCallbackFunc(uv_async_t *handle) {
        HandleScope handle_scope(Isolate::GetCurrent());
        GoThreadData*  goThreadData = (GoThreadData *) handle->data;
        // printf("%s___%d__%s
    ", __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获取进度值并回调给js
    }
    
    // uv异步句柄关闭回调
    void close_cb(uv_handle_t* handle)
    {
        // printf("close the async handle!
    ");
    }
    
    // 这个函数传给golang调用, 当golang通知js有进度更新时这里会执行,extra参数是一个GoThreadData, 用来区分是那一次调用的回调, 可以将GoThreadData理解为go函数调用上下文
    void goCallback(char * extra, char * arg) {
        // printf("%s: %d
    ", __FUNCTION__,  (int)uv_thread_self());
        GoThreadData* data = (GoThreadData *) extra;
        delete data->progress;
        data->progress = arg; // 把进度信息放到上下文当中
        // printf("%d:%s---%s----%s
    ",__LINE__, arg, data->func, data->progress);
        uv_async_send(data->progressReq); // 通知主线程, 这里会导致上面的progressCallbackFunc执行
    }
    
    void * goLib = nullptr; // 打开的gofun.so的句柄
    
    typedef char* (*GoFunc)(char* p0); // go同步函数和不带进度的异步函数
    typedef char* (*GoFuncWithProgress)(char* p0, void (*goCallback) (char* extra, char * arg), char * data); // go带进度回调的异步函数
    
    map<string, GoFunc> loadedGoFunc; // 一个map用来存储已经加载啦那些函数
    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(); // 覆盖函数
        args.GetReturnValue().Set(true); // 返回true给js
    }
    
    // 释放go函数调用上下文结构体的内存
    void freeGoThreadData(GoThreadData* data) {
        delete data->result;
        delete data->progress;
        delete data->arg;
        delete data->error;
        delete data;
    }
    
    // 由libuv在主线程中进行调用, 当go函数返回时,这里会被调用
    void afterGoTread (uv_work_t* req, int status) {
        // printf("%s: %d
    ", __FUNCTION__,  (int)uv_thread_self());
        auto * goThreadData = (GoThreadData*) req->data;
        HandleScope handle_scope(Isolate::GetCurrent());// 这里是必须的,调用js函数需要一个handle scope
        if (goThreadData->hasError) { // 如果有错误, 生成一个错误实例并传给js错误回调
            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;
        }
        // 没有错误, 把结果回调给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进度的事件删除, 不然这个事件会一直存在时间循环中, node进程也不会退出
        }
        // 释放内存
        freeGoThreadData(goThreadData);
    }
    
    
    
    
    // 工作线程, 在这个函数中调用go
    void callGoThread(uv_work_t* req)
    {
        // 从uv_work_t的结构体中获取我们定义的入参结构
        auto * goThreadData = (GoThreadData*) req->data;
    
        // printf("%s: %d
    ", __FUNCTION__,  (int)uv_thread_self());
        // 检查内核是否加载
        if (!goLib) {
            goThreadData->hasError = true;
            String::NewFromUtf8(goThreadData->isolate, "请先加载内核");
            goThreadData->error = "请先加载内核";
            return;
        }
    
        if (!goThreadData->isProgress) {
            // 检查函数是否加载
            if (! loadedGoFunc[goThreadData->func]) {
                auto goFunc = (GoFunc) dlsym(goLib, goThreadData->func);
                if(!goFunc)
                {
                    goThreadData->hasError = true;
                    goThreadData->error = "函数加载失败";
                    return;
                }
                // printf("loaded %s
    ", goThreadData->func);
                loadedGoFunc[goThreadData->func] = goFunc;
            }
    
            // 调用go函数
            GoFunc func = loadedGoFunc[goThreadData->func];
            char * result = func(goThreadData->arg);
            // printf("%d:%s
    -----------------------------
    ", __LINE__, result);
            // printf("%d:%s
    -----------------------------
    ", __LINE__, goThreadData->arg);
            goThreadData->result = result;
            return;
        }
    
        // 有progress回调函数的
        // 检查函数是否加载
        if (! loadedGoFuncWithProgress[goThreadData->func]) {
            auto goFunc = (GoFuncWithProgress) dlsym(goLib, goThreadData->func);
            if(!goFunc)
            {
                goThreadData->hasError = true;
                goThreadData->error = "函数加载失败";
                return;
            }
            // printf("loaded %s
    ", goThreadData->func);
            loadedGoFuncWithProgress[goThreadData->func] = goFunc;
        }
    
        // 调用go函数
        GoFuncWithProgress func = loadedGoFuncWithProgress[goThreadData->func];
        char * result = func(goThreadData->arg, goCallback, (char*) goThreadData);
        // printf("%d:%s
    -----------------------------
    ", __LINE__, result);
        // printf("%d:%s
    -----------------------------
    ", __LINE__, goThreadData->arg);
        goThreadData->result = result;
    }
    
    
    // 暴露给js的,用来调用go的非同步函数(同步只是相对js而言, 实际上go函数还是同步执行的)
    void callGoAsync(const FunctionCallbackInfo<Value>& args) {
        // printf("%s: %d
    ", __FUNCTION__,  (int)uv_thread_self());
    
        Isolate* isolate = args.GetIsolate();
    
        // 检查传入的参数的个数
        if (args.Length() < 3 || (
                !args[0]->IsString()
                || !args[1]->IsString()
                || !args[2]->IsFunction()
                || !args[3]->IsFunction()
        )) {
            // 抛出一个错误并传回到 JavaScript
            isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "调用格式: 函数名称, JSON参数, 成功回调, 错误回调")));
            return;
        }
        // 参数格式化, 构造线程数据
        auto goThreadData = new GoThreadData;
    
       // 有第5个参数, 说明是调用有进度回调的go函数
        if (args.Length() >= 5) {
            if (!args[4]->IsFunction()) {
                isolate->ThrowException(Exception::TypeError(
                        String::NewFromUtf8(isolate, "如果有第5个参数, 请传入Progress回调")));
                return;
            } else {
                goThreadData->isProgress = true;
                goThreadData->onProgress.Reset(isolate, Local<Function>::Cast(args[4]));
            }
        }
    
        // go调用上下文的初始化
        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);
    
        // 调用libuv实现多线程
        auto req = new uv_work_t();
        req->data = goThreadData;
    
        // 如果是有进度回调的需要注册一个异步事件, 以便在子线程回调js
        if (goThreadData->isProgress) {
            goThreadData->progressReq = new uv_async_t();
            goThreadData->progressReq->data = (void *) goThreadData;
            uv_async_init(uv_default_loop(), goThreadData->progressReq, progressCallbackFunc);
        }
    
        // 调用libuv的线程处理函数
        uv_queue_work(uv_default_loop(), req, callGoThread, afterGoTread);
    
    }
    
    
    // 模块初始化, 注册暴露给js的函数
    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)
        })
    })
    
    
  • 相关阅读:
    linux 学习笔记
    linux 子系统折腾记 (三)
    linux子系统折腾记 (二)
    windows linux 子系统折腾记
    会计学习笔记(非专业)
    linux 大冒险
    coreRT 和 Native 编译netcore AOT程序
    dotnet core如何编译exe
    win10的hyper-v共享文件夹
    packagereference 里面的资产是怎么回事?
  • 原文地址:https://www.cnblogs.com/answercard/p/11511068.html
Copyright © 2011-2022 走看看