Java调用Golang生成的动态库(dll,so)

1. 环境准备

A. GCC

在控制台中输入

gcc -v

如果提示命令未找到,那么说明你的计算机中还没有gcc,去安装一个吧,gcc官方网站:https://gcc.gnu.org/ 如果从来没有安装过gcc的朋友可以直接安装win-build,可以帮你快速的安装 官方网站:http://mingw-w64.org/doku.php/download/win-builds

2. 编写go程序

我们这里只是编写一个简单的计算加法的程序,接受两个整数,然后计算他们的和,并返回。 在这里,我们将文件命名为libhello.go

package main

import "C"

//export Sum
func Sum(a int, b int) int {
    return a + b
}

func main() {
}

注意,即使是要编译成动态库,也要有main函数,上面的import "C"一定要有 而且一定要有注释

//export Sum

经测试,如果没有这个导出的DLL库中找不到对应的函数

3. 编译go程序

首先,将控制台的所在目录切换到go程序的所在目录,即libhello.go所在目录

A. Windows动态库

执行如下命令生成DLL动态链接库:

go build -buildmode=c-shared -o libhello.dll .\libhello.go

如果控制台没有报错,那么会在当前路径下生成libhello.dll文件

B. Linux/Unix/macOS动态库

执行如下命令生成SO动态库:

go build -buildmode=c-shared -o libhello.so .\libhello.go

4. 在java中调用

A. JNA的引用

Java调用Native的动态库有两种方式,JNI和JNA,JNA是Oracle最新推出的与Native交互的方式,具体介绍我就不多说了,引用百度百科的连接:https://baike.baidu.com/item/JNA/8637274?fr=aladdin,有需要的朋友可以去看看。 在这里,我们使用JNA的方式,JNI的方式基本废弃,除非有特殊需要,在这里不多说,有需要可以联系我讨论。 新建Java工程,我使用的是Maven做包管理,所以直接引用JNA的依赖:

<dependency>
      <groupId>net.java.dev.jna</groupId>
      <artifactId>jna</artifactId>
      <version>4.5.2</version>
</dependency>

如果你没有使用包管理工具,可以直接下载Jar文件引入,下载地址也贴一下吧,也是4.5.2版本的: http://central.maven.org/maven2/net/java/dev/jna/jna/4.5.2/jna-4.5.2.jar

B. 创建接口

我们需要创建一个interface来映射DLL中的函数,之后我们可以通过interface的实例来访问DLL中的函数。

package cn.lemonit.robot.runner.executor;

import com.sun.jna.Library;
import com.sun.jna.Native;

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    int Sum(int a, int b);
}

注意,Sum是函数名,一定要与Go中事先写好的函数名保持一致 Native.loadLibrary()的第一个参数是一个字符串,要加载的动态库的名称或全路径,后面不需要加.dll或者.so的后缀。第二个参数为interface的类名称。

C. 调用

我们新建一个App类,作为main方法的入口类,在main方法中不需要多余的操作,只需要调用即可,在这里我们调用Sum方法,同时传如222 , 333,可以看到控制台输出:555

package cn.lemonit.robot.runner.executor;

public class App {

    public static void main(String[] args) {
        System.out.println(LibHello.INSTANCE.Sum(222, 333));
    }
}

大功告成,我终于玩通了Java调用Go程序!!!! ???不对劲,有点太过于幸灾乐祸了,往下继续

5. 参数中包含字符串

A. 我真的大功告成了吗?

我们的程序总不能只传数值型的参数吧,我们把GO程序改一下,换成一个一字符串作为参数的函数,接受一个字符串参数,然后从控制台输出:hello: xxx,如下:

package main

import "fmt"

//export Hello
func Hello(msg string) {
    fmt.Print("hello: " + msg)
}

func main() {
}

按照上面2.B步骤中的写法,我们将java的LibHello接口改成这个样子:

package cn.lemonit.robot.runner.executor;

import com.sun.jna.Library;
import com.sun.jna.Native;

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    void Hello(String msg);
}

接下来,我们调用这个接口,将 2.C 中的启动入口类App代码改成这样:

package cn.lemonit.robot.runner.executor;

public class App {

    public static void main(String[] args) {
        LibHello.INSTANCE.Hello("LemonIT.CN");
    }
}

运行起来,咦?报错了???

fatal error: string concatenation too long

goroutine 17 [running, locked to thread]:
runtime.throw(0x644c1d4f, 0x1d)
  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (runtime报错,没有意义,不贴了)

这是怎么回事,我传的是一个很标准的String啊,怎么会报错呢? 在一阵无头绪中,发现刚才在调用go build -buildmode=c-shared -o libhello.dll .\libhello.go命令的时候在文件夹中除了libhello.dll被生成之外,还生成了一个libhello.h文件!!!这不是C的头文件么?出于好奇,打开看看有什么高大上的东西,这一打开还真是吓到我了:

/* Created by "go tool cgo" - DO NOT EDIT. */

/* package command-line-arguments */

#line 1 "cgo-builtin-prolog"

#include <stddef.h> /* for ptrdiff_t below */

#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H

typedef struct { const char *p; ptrdiff_t n; } _GoString_;

#endif

/* Start of preamble from import "C" comments.  */
/* End of preamble from import "C" comments.  */

/* Start of boilerplate cgo prologue.  */
#line 1 "cgo-gcc-export-header-prolog"

#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H

typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef __SIZE_TYPE__ GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;

/*
  static assertion to make sure the file is being used on architecture
  at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];

typedef _GoString_ GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

#endif

/* End of boilerplate cgo prologue.  */

#ifdef __cplusplus
extern "C" {
#endif

extern void Hello(GoString p0);

#ifdef __cplusplus
}
#endif

这么大一篇子,往下翻翻翻,找到了我们的Hello函数的定义:

extern void Hello(GoString p0);

发现问题了,人家参数要的事GoString,而我们传的是Java的String,肯定类型不一致啊。那GoString是个什么东西呢,我该给他传什么?往上翻,找到了这么两行代码:

typedef struct { const char *p; ptrdiff_t n; } _GoString_;
// .....
typedef _GoString_ GoString;

嗯嗯嗯,看来这个GoString不过就是个C里面的结构体罢了,结构体里面一个char *一个ptrdiff_t,看来我们用java调用程序的时候,构造个这么样的结构体给他传进来应该就行了,好了,有思路了,开始折腾。

B. 创建GoString!

我们首先用JNA构建一个C的结构体类型,那么问题来了,JNA中char *可以直接用java的String来代替,那么ptrdiff_t这个玩意……有点无语,这是啥啊?经过一顿操作百度和谷歌,终于知道了,这个类型实际上是两个内存地址之间的距离的值,数据类型实际上就是C中的long int,在这里他表示的是字符串char *的长度,也就是字符串的长度呗~,知道这个就好办了,我们在Java中直接用long类型来代替它。 我们新建一个GoString类来对应C中的GoString结构体,也就是Go程序中的string,这块得说一下,有些人可能没有用过JNA,在JNA中若想定义一个结构体,需要创建一个类继承自com.sun.jna.Structure,熟悉C的人应该知道(不知道也没关系),向C中传值通常有两种,一种是传引用(就是传指针类型),一种是传真实值,在JNA里面做的话我们通常在这个结构体类中创建两个静态的内部类,这两个内部类继承自这个结构体类,并实现Structure.ByValue和Structure.ByReference接口,其中ByValue就是传真实值时候用的,ByReference就是传引用的时候用的,综上所述,我们的GoString类就应该长成这个样子:

package cn.lemonit.robot.runner.executor;

import com.sun.jna.Structure;

import java.util.ArrayList;
import java.util.List;

public class GoString extends Structure {

    public String str;
    public long length;

    public GoString() {
    }

    public GoString(String str) {
        this.str = str;
        this.length = str.length();
    }

    @Override
    protected List<String> getFieldOrder() {
        List<String> fields = new ArrayList<>();
        fields.add("str");
        fields.add("length");
        return fields;
    }

    public static class ByValue extends GoString implements Structure.ByValue {
        public ByValue() {
        }

        public ByValue(String str) {
            super(str);
        }
    }

    public static class ByReference extends GoString implements Structure.ByReference {
        public ByReference() {
        }

        public ByReference(String str) {
            super(str);
        }
    }
}

可以发现,我们重写了一个getFieldOrder方法,在里面新建一个list,然后把两个属性名作为字符串放到里面,然后当做返回值返回了。这个操作实际是为了告诉JNA,我这两个变量和C结构体中的变量是怎么个对应关系的,我们再来回顾一下刚才libhello.h中定义的GoString结构体(其实是省着你再往上翻看,费劲,直接粘出来方便你看):

typedef struct { const char *p; ptrdiff_t n; } _GoString_;

我们的字符串叫str,而char *的名称是p,我们的字符串长度叫length,而结构体中叫n,JNA又不是人工智能框架,肯定猜不出来你想把str对应到p,length想对应到n,所以我们在这里通过list的形式把字段名在list中排一个顺序,告诉JNA,我的str想对应结构体的第一个属性,length想对应结构体的第二个属性。(你可以试试,让fields.add的顺序调换一下,肯定会出问题)。

C. 有了GoString!我又可以幸灾乐祸了!???

好了,GoString有了,万事俱备,只欠东风了!用一把,我们把刚才0x05.A中的LibHello类改成这样:

package cn.lemonit.robot.runner.executor;

import com.sun.jna.Library;
import com.sun.jna.Native;

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    void Hello(GoString.ByValue msg);
}

App入口类代码改成这样:

package cn.lemonit.robot.runner.executor;

public class App {
    public static void main(String[] args) {
        LibHello.INSTANCE.Hello(new GoString.ByValue("LemonIT.CN"));
    }
}

运行!控制台成功输出:

hello: LemonIT.CN

哈哈哈!成功了,有点小激动!把代码发给朋友们看!!!有一个朋友问我,你这Hello函数的结果能不能不在Go中的控制台打印,而是在Java中打印到控制台?额……我犹豫了一下,应该能吧……!

6. 返回值中包含字符串

A. 做一个小实验~

我们把5中的Go函数Hello改一下,让结果通过返回值返回,而不是直接在控制台打印,变成这样滴:

package main

import "C"

//export Hello
func Hello(msg string) string{
    return "hello:" + msg
}

func main() {
}

既然返回值也是string,那JNA这边也得小改一波,把0x05.C中的LibHello类改成这样:

package cn.lemonit.robot.runner.executor;

import com.sun.jna.Library;
import com.sun.jna.Native;

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    GoString.ByValue Hello(GoString.ByValue msg);
}

运行入口类App也对应修改一下:

package cn.lemonit.robot.runner.executor;

public class App {
    public static void main(String[] args) {
        System.out.println(LibHello.INSTANCE.Hello(new GoString.ByValue("LemonIT.CN")).str);
    }
}

大功告成,运行一下!

panic: runtime error: cgo result has Go pointer

goroutine 17 [running, locked to thread]:
main._cgoexpwrap_b02601c1465e_Hello.func1(0xc04203deb8)
    _cgo_gotypes.go:59 +0x6c
main._cgoexpwrap_b02601c1465e_Hello(0xbe3ce0, 0xa, 0xc042008050, 0x10)
    _cgo_gotypes.go:61 +0xa1

嗯?这啥?我的LemonIT.CN呢?在控制台中并没有找到啊!有点让人发狂,怎么一步一个坎~不过想想比尔盖茨,我还是决定做一名脾气好的程序员,慢慢研究吧。

B. 事情总有解决办法!(车到山前必有路,有路必有丰田车)

虽然没有LemonIT.CN,但是看控制台中的error,cgo result has Go pointer,还是找到了一丝线索。又开始一顿操作百度和谷歌。原来,Go有自己的GC(垃圾回收,不解释),通俗点说就是我Go语言的指针你们其他语言别想用!额,那咋整!急的我连大学时候的课堂笔记都翻出来了。无意中看到了当时写的借助JNA与C通信,C中将char *返回给Java,然后Java使用String即可接收。嗯,嗯?这条咋忘了呢?哈哈哈,岂不是我把Go中的string转成C的char *返回就可以了?好了,让我们试上那么一试,把刚才的Go中的Hello函数再次修改一波:

package main

import "C"

//export Hello
func Hello(msg string) *C.char{
    return C.CString("hello : " + msg)  
}

func main() {
}

同样滴,我们的JNA这边也得改一改,把LibHello类修改成这样:

package cn.lemonit.robot.runner.executor;

import com.sun.jna.Library;
import com.sun.jna.Native;

public interface LibHello extends Library {
    LibHello INSTANCE = (LibHello) Native.loadLibrary("E:/workspace/libhello", LibHello.class);

    String Hello(GoString.ByValue msg);
}

LibHello既然改了,那么入口类App也得对应修改:

package cn.lemonit.robot.runner.executor;

public class App {
    public static void main(String[] args) {
        System.out.println(LibHello.INSTANCE.Hello(new GoString.ByValue("LemonIT.CN")));
    }
}

好了好了好了,运行:

hello : LemonIT.CN

终于输出出来了!

7. 总结

这个Go和Java的交互刚刚走了这一小小步就一步一个坎,看来真不能随便的幸灾乐祸啊!!还得谦虚,路才能越走越远。虽然费了这么大劲就解决了这么点小事,但是Go语言的优势是很大的,还是很值得我来折腾的,相信能读到这里的朋友也是对Go语言非常的喜爱,大家一起加油吧,欢迎各位大佬来指正批评~

感谢作者:LemonITCN
原文链接:https://studygolang.com/articles/13646#reply1

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 简介: JNA全称:Java Native Access,是一款在JNI层做了封装,为了简捷方便让开发者调用动态库...
    墨染草阅读 4,579评论 0 0
  • 因为动态库文件是使用c或是c++编写的,所以在java中是不能直接调用动态库文件的,作为一种跨平台的编程语言,ja...
    乌云老思阅读 12,853评论 2 1
  • 动态调用动态库方法c/c++linuxwindows 关于动态调用动态库方法说明 一、 动态库概述 1、 动态库的...
    KINGZ1993阅读 13,894评论 0 10
  • 也许每个人的轨迹都并没有我想象的那么不同,慢慢地成长,后来就知道了原来成长后就是这个样子,在漫长的岁月里,...
    如果我变成回阅读 207评论 0 0
  • 秋天的雨,细细的,绵绵的,一滴一滴落在梧桐叶上,又滑落到地上,消失在一洼洼的泥池里,听着窗外嘀嗒嘀嗒的雨声,脑子里...
    溪山清静阅读 211评论 0 0