Since Fyne version `v2.3.0` it has been possible to call native code in the Android JVM using a new RunNative function. This feature opens access to any of the APIs you might want to use from the official Android SDK. This blog post shows how some Go and C code can be used to make native calls in your application.

Structuring the code

In this example we assume that you want to create a feature that looks up the device name – this function lookupName could be used to pass into a NewLabel or a Label.SetText to display device information in your app. It is recommended to set up 3 files to keep this easy to read – two .go files, one for use on Android and a fallback for other devices, and a .c file that includes the non-Go code for Android lookups.

  • lookup_android.go – This file contains the Go code for making the request to android C code
  • lookup_other.go – The fallback file that does not need any Android features for other devices
  • lookup_android.c – C code for connecting to the java virtual machine and calling Android SDK.

Next we look at what each of these files should contain.

Creating the API

Firstly we should write the Go code that we will use to get the data from Android. It opens with a build statement in case you decide to use a different filename (something ending _android.go will only be compiled for an Android device anyway).

As you can see this introduces some CGo code that declares a androidName method that will be implemented later. The key here is that we use driver.RunNative to execute our Go in the context of the Java runtime. We use the Context passed in and asset that it is an AndroidContext which provides the required pointers that we pass into our android code. Ignoring the pointer manipulation this is a relatively simple function.

Note that we also need to convert the C const char * type of string to a Go string using the C.GoString function.

//go:build android

package main

import "fyne.io/fyne/v2/driver"

/*
#include <stdlib.h>

const char *androidName(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx);
*/
import "C"

func lookupName() (name string) {
    driver.RunNative(func(ctx interface{}) error {
        ac := ctx.(*driver.AndroidContext)

        str := C.androidName(C.uintptr_t(ac.VM), C.uintptr_t(ac.Env), C.uintptr_t(ac.Ctx))
        name = C.GoString(str)
        return nil
    })

    return name
}

Now we need a fallback that will run if it’s a different device type we are running on. The contents of this file are a simple empty method that matches the one we created above. There is a build directive at the top that ensures this will only build on platforms that are not android.

//go:build !android

package main

func lookupName() (name string) {
    return "unsupported"
}

Connecting to the Android SDK

The file we need is for the C code we will use to connect into the Android runtime. If you have ever programmed against Java’s JNI (native invocation) API This will look familiar – but let’s step though this code for everyone else.

This is split into two snippets for ease of understanding. This first adds the headers and C function for accessing the name of the device. We use the java runtime pointers to connect to the JVM and access functionality of the android.os.Build class, in this case so we can read the static field called MODEL. We use these references to read the field using GetStaticObjectField which will return a java string object. To return the data in a C format we then call getCString which gets the data out of the java object and returns a regular const char * type which C uses for strings.

//go:build android

#include <jni.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

// helper functions go here

const char *androidName(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
    JNIEnv *env = (JNIEnv*)jni_env;

    // look up model from Build class
    jclass buildClass = find_class(env, "android/os/Build");
    jfieldID modelFieldID = (*env)->GetStaticFieldID(env, buildClass, "MODEL", "Ljava/lang/String;");
    jstring model = (*env)->GetStaticObjectField(env, buildClass, modelFieldID);

    // convert to a C string
    return getCString(jni_env, ctx, model);
}

The code above is relatively easy to understand, but it requires some supporting functions which we need to add at the top of the file where “helper functions go here” was written… There are two functions, the find_class returns a class instance for a given class identifier. From here we are able to use the JNI functions shown above to access fields or methods on a class definition. Lastly we add the string conversion utility which uses familiar JNI function calls to convert a string object to a standard C string.

static jclass find_class(JNIEnv *env, const char *class_name) {
    jclass clazz = (*env)->FindClass(env, class_name);
    if (clazz == NULL) {
        (*env)->ExceptionClear(env);
        printf("cannot find %s", class_name);
        return NULL;
    }
    return clazz;
}

const char* getCString(uintptr_t jni_env, uintptr_t ctx, jstring str) {
    JNIEnv *env = (JNIEnv*)jni_env;

    const char *chars = (*env)->GetStringUTFChars(env, str, NULL);

    const char *copy = strdup(chars);
    (*env)->ReleaseStringUTFChars(env, str, chars);
    return copy;
}

And that’s all there is to… It is unfortunately not pure Go, but it does demonstrate that your mobile apps built with Go and Fyne can access platform specific APIs even when you need to use the java runtime that Android is built with.

Discover more from Fyne Labs

Subscribe now to keep reading and get access to the full archive.

Continue reading