很明显,要实现在Android上运行Go的代码,需要分两步走:
- 将Go代码编译成动态链接库,也就是.so库
- android 通过NDK调用so库,生成新的so库,通过JNI调用
1. Go代码编译成动态链接库
将Go代码编译成动态链接库,可以通过下面的命令:
1
go build -buildmode=c-shared -o libxx.so xxx.go
其中,-buildmode=c-shared
表示编译模式为 动态链接, -o libxx.so
为生成指定的so,xxx.go
表示要进行编译的 go代码。
本文的编译环境是在windows下进行交叉编译
看似一行命令就行了,但是坑却很深:
- 无法生成so库?
- 生成了so库,但是没有头文件?
- 搞定了上面的问题之后,尝试c代码调用so库一直失败?
- android无法调用so库?设置go build参数,交叉编译linux下的so库失败?
针对以上问题,下面逐个进行分析。
1.1 无法生成so库
针对无法编译成so库的问题,我总结了几个可能的错误:
提示
-buildmode=c-shared not supported on windows/amd64
-buildmode=c-shared
对go 版本有要求,go version >= 1.10提示
can't load package: package .: build constraints exclude all Go files in
需要先设置go env:
set CGO_ENABLED=1
提示
unimplemented: 64-bit mode not compiled in
不支持64位的编译,修改GOARCH:
set GOARCH=386
1.2 生成了so库,但是没有头文件?
首先,看下go代码:
1
2
3
4
5
6
7
8
9
package main
import "C"
func hello(message string) string {
return "hello," + message
}
func main() {}
将go代码编译成c so库,需要import “C”。
很简单的一段代码,编译之后成功生成了so文件,却没有头文件。。。研究了半天,发现:只有通过注释 export func
,才会生成头文件(OS: 还有这种操作?!!)。修改下代码:
1
2
3
4
5
6
7
8
9
10
package main
import "C"
//export hello
func hello(message string) string {
return "hello," + message
}
func main() {}
重新跑一遍命令,发现so库跟头文件都生成了,第一个问题顺利解决。
生成的内容:
libgodemo.so
libgodemo.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
/* Code generated by cmd/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 GoInt32 GoInt; typedef GoUint32 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_32_bit_pointer_matching_GoInt[sizeof(void*)==32/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 GoString hello(GoString p0); #ifdef __cplusplus } #endif
1.3 C代码调用so库一直失败?
原本生成了so库和头文件之后,直接拿去NDK生成so库的,但是发现一直失败,就怀疑是否是so库本身有问题,便决定先用C代码调一遍看看。
这里需要gcc编译环境,没有的话需要先安装
c代码调用so库:
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include "libgodemo.h"
void main() {
GoString gostring;
gostring.p = "ejin";
gostring.n = 4;
printf("%s", hello(gostring).p);
}
gcc命令如下:
1
gcc test.c -L ./ -lgodemo -o test.exe
根据网上的描述,调用so库需要-L -l, 但是这种命令方式一直不行。最后,换成下面这种写法:
1
gcc test.c libgodemo.so -o test.exe
终于,成功的生成了 test.exe
,迫不及待的在命令行上运行 test.exe
,发现报错,真是命运坎坷啊!!!错误如下:
1
2
3
4
5
6
7
panic: runtime error: cgo result has Go pointer
goroutine 17 [running, locked to thread]:
main._cgoexpwrap_ebd98c1bdb5c_hello.func1(0x11433f6c)
_cgo_gotypes.go:46 +0x50
main._cgoexpwrap_ebd98c1bdb5c_hello(0x405064, 0x3, 0x11456010, 0x9)
_cgo_gotypes.go:48 +0x8a
仔细看下上面的头文件,发现在go代码中的string
最后转成了 GoString
,可能是go中的基本类型与c语言中的基本类型转换的问题。针对上面的错误,我还没有能够弄清楚,但是可以通过下面的写法来规避这种错误:
1
2
3
4
5
6
7
8
9
10
11
package main
import "C"
//export hello
func hello(message string) *C.char {
var s = C.CString("hello," + message)
return s
}
func main() {}
就是不直接返回 string
, 而是*C.char
, 也就是C中的 char*
。
重新编译一遍Go, 生成的头文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/* Code generated by cmd/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 GoInt32 GoInt;
typedef GoUint32 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_32_bit_pointer_matching_GoInt[sizeof(void*)==32/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 char* hello(GoString p0);
#ifdef __cplusplus
}
#endif
头文件中 hello
返回的类型由原来的 GoString
变成了 char*
。
更改一下c代码:
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include "libgodemo.h"
void main() {
GoString gostring;
gostring.p = "ejin";
gostring.n = 4;
printf("%s", hello(gostring));
}
gcc重新编译一遍,生成 test.exe
, 并运行:
1
2
D:\go_c_jni>test.exe
hello,ejin
终于成功了。。。激动地热泪盈眶。。。
1.4 android无法调用so库?设置go build参数,交叉编译linux下的so库失败?
因为编译环境是 windows
,所以肯定是不行的。修改go build的 env:
1
2
set GOARCH=arm
set GOOS=linux
修改完成后,可以通过
go env
查看详细参数配置
重新运行上面的 go build -buildmode....
命令,居然又报错了。。。
1
2
# runtime/cgo
gcc: error: unrecognized command line option '-marm'; did you mean '-mabm'?
又跑去google, 发现需要将 go env cc
设置为 arm-linux-gnueabi-gcc
, 需要一个专业的交叉编译的gcc才行。修改go env:
1
set CC=arm-linux-gnueabi-gcc
重新运行命令,发现没有装 arm-linux-gnueabi-gcc
。接着去下载,官网下载地址是:arm-linux-gnueabi-gcc。下载安装完成之后,配置下环境变量Path, 重新运行命令 go build ...
。
终于,还是成功了。。。(一口老血喷出来)。。。
2. NDK生成so库,JNI调用
2.1 NDK调用还是不成功?
经历重重艰险,成功的生成了so库,万里长征走了一半,希望下面能够一帆风顺。。。
关于用
NDK
生成so库,不了解的可以参考下:NDK开发总结。
但是,,,NDK调用该动态链接库总是失败。。。
猜测原因:so库的编译环境是 arm/linux, gcc是arm-linux-guneabi-gcc。而android平台还是有差异,重新设置go build环境参数:
1
2
3
4
5
set GOARCH=arm
set GOOS=android
set CC=arm-linux-androideabi-gcc
set CGO_ENABLED=1
go build -buildmode=c-shared -o libgodemo.so godemo.go
其中 cc为 arm-linux-androideabi-gcc
, 它是NDK自带的gcc,需要设置到 PATH
中去。在NDK中的地址是:
toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin
。
设置完调用 go build ...
还是报错:
1
2
3
4
5
# runtime/cgo
_cgo_export.c:3:20: fatal error: stdlib.h: No such file or directory
#include <stdlib.h>
^
compilation terminated.
曾一度卡在这里,最后还是找到了解决方案:还需要设置 --sysroot
以及 CGO_LDFLAGS
:
1
2
set CGO_LDFLAGS=--sysroot=%NDK_ROOT%/platforms/android-21/arch-arm
set CC=arm-linux-androideabi-gcc --sysroot=%NDK_ROOT%\platforms\android-21\arch-arm
android
平台的so库终于成功生成了(喜大普奔)。。。
2.2 Cmake & JNI
既然so库正确了,那后面就顺畅了。。。
native方法
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.ejin.ndk;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("godemo");
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI("ejin"));
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI(String s);
}
native-lib.c
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <jni.h>
#include <string.h>
#include "include/libgodemo.h"
JNIEXPORT jstring JNICALL
Java_com_ejin_ndk_MainActivity_stringFromJNI(JNIEnv *env, jobject instance, jstring s_) {
const char *s = (*env)->GetStringUTFChars(env, s_, 0);
GoString goString;
goString.p = s;
goString.n = strlen(s);
return (*env)->NewStringUTF(env, hello(goString));
}
CMakeLists.txt
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
#设置cmake支持的最小版本
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/jni/${ANDROID_ABI})
#头文件路径
include_directories("src/main/jniLibs/include")
#配置文件
file(GLOB native_srcs "src/main/jniLibs/*.c")
#配置生成的so库的配置。
#第一次参数:生产的so库名称
#第二个参数:生成的so库的类型,SHARED代表动态链接库.so,STATIC代表静态库.a
#第三个开始的参数:生成库所包括的代码文件。可以枚举,也可以用上面定义的文件集合:{native_srcs}
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${native_srcs} )
add_library(godemo
SHARED
IMPORTED
)
set_target_properties(godemo
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libgodemo.so
)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
#查找库。
#第一个参数:定义变量名,表示查找的库
#第二个参数:需要查找的本地库
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
#关联本地库到目标生成库
target_link_libraries( # Specifies the target library.
native-lib
${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libgodemo.so
# Links the target library to the log library
# included in the NDK.
${log-lib} )
最后,效果图:
2.3 项目地址
github项目地址:goland-android