很明显,要实现在Android上运行Go的代码,需要分两步走:

  1. 将Go代码编译成动态链接库,也就是.so库
  2. android 通过NDK调用so库,生成新的so库,通过JNI调用


1. Go代码编译成动态链接库

将Go代码编译成动态链接库,可以通过下面的命令:

go build -buildmode=c-shared -o libxx.so xxx.go

其中,-buildmode=c-shared 表示编译模式为 动态链接, -o libxx.so 为生成指定的so,xxx.go 表示要进行编译的 go代码。

本文的编译环境是在windows下进行交叉编译

看似一行命令就行了,但是坑却很深:

  1. 无法生成so库?
  2. 生成了so库,但是没有头文件?
  3. 搞定了上面的问题之后,尝试c代码调用so库一直失败?
  4. 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代码:

package main

import "C"

func hello(message string) string {
	return "hello," + message
}

func main() {}

将go代码编译成c so库,需要import “C”。

很简单的一段代码,编译之后成功生成了so文件,却没有头文件。。。研究了半天,发现:只有通过注释 export func ,才会生成头文件(OS: 还有这种操作?!!)。修改下代码:

package main

import "C"

//export hello
func hello(message string) string {
	return "hello," + message
}

func main() {}

重新跑一遍命令,发现so库跟头文件都生成了,第一个问题顺利解决。

生成的内容:

  • libgodemo.so

  • libgodemo.h

    /* 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库:

#include <stdio.h>
#include "libgodemo.h"

void main() {
	GoString gostring;
	gostring.p = "ejin";
	gostring.n = 4;
	printf("%s", hello(gostring).p);
}

gcc命令如下:

gcc test.c -L ./ -lgodemo -o test.exe

根据网上的描述,调用so库需要-L -l, 但是这种命令方式一直不行。最后,换成下面这种写法:

gcc test.c libgodemo.so -o test.exe

终于,成功的生成了 test.exe,迫不及待的在命令行上运行 test.exe,发现报错,真是命运坎坷啊!!!错误如下:

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语言中的基本类型转换的问题。针对上面的错误,我还没有能够弄清楚,但是可以通过下面的写法来规避这种错误:

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, 生成的头文件如下:

/* 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代码:

#include <stdio.h>
#include "libgodemo.h"

void main() {
	GoString gostring;
	gostring.p = "ejin";
	gostring.n = 4;
	printf("%s", hello(gostring));
}

gcc重新编译一遍,生成 test.exe, 并运行:

D:\go_c_jni>test.exe
hello,ejin

终于成功了。。。激动地热泪盈眶。。。


1.4 android无法调用so库?设置go build参数,交叉编译linux下的so库失败?

因为编译环境是 windows,所以肯定是不行的。修改go build的 env:

set GOARCH=arm
set GOOS=linux

修改完成后,可以通过 go env 查看详细参数配置

重新运行上面的 go build -buildmode.... 命令,居然又报错了。。。

# runtime/cgo
gcc: error: unrecognized command line option '-marm'; did you mean '-mabm'?

又跑去google, 发现需要将 go env cc 设置为 arm-linux-gnueabi-gcc, 需要一个专业的交叉编译的gcc才行。修改go env:

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环境参数:

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 ... 还是报错:

# runtime/cgo
_cgo_export.c:3:20: fatal error: stdlib.h: No such file or directory
 #include <stdlib.h>
                    ^
compilation terminated.

曾一度卡在这里,最后还是找到了解决方案:还需要设置 --sysroot 以及 CGO_LDFLAGS:

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方法

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:

#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

# 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