DEV Community

Jhin Lee
Jhin Lee

Posted on

How to use GoLang in Flutter Application - Golang FFI

GoLang is a very powerful language that is easy to use. Writing a GoLang program and compiling it to a binary file is very easy. Golang is very useful in writing high-performance and low-latency applications, but it needs improvement in GUI. Flutter is a compelling cross-platform UI framework. It allows you to write a beautiful application effortlessly. Mixing GoLang and Flutter is an excellent idea to write a high-performance and beautiful application.

In this article, we will see how to compile a GoLang-based library and create a Flutter plugin package for a Flutter application.

What is FFI?

FFI stands for Foreign Function Interface. It is a way to call functions written in other languages from Dart. FFI helps use libraries written in other languages in a Flutter application. Flutter supports FFI for Android, iOS, Linux, macOS, and Windows. In this article, we will see how to use FFI.

In Dart, the programming language used to write Flutter applications, we can use the dart:ffi package to use FFI. The dart:ffi package is used to call functions written in other languages from Dart. We can use the dart:ffi package to call functions written in C, C++, or Rust. In this article, we will see how to use the dart:ffi package to call functions in GoLang.

Create a plugin project for Flutter

The Flutter plugin can contain Dart and platform-specific code written in Kotlin, Swift, Objective-C, or GoLang. We can use a Flutter plugin to add functionality to a Flutter application.

The below command will create a Flutter plugin project named native_add. The native_add project will support Android, iOS, Linux, macOS, and Windows. The native_add project will have a lib folder that contains the Dart code and an example folder that contains a Flutter application to test the plugin.

 $ flutter create --platforms=android,ios,macos,windows,linux --template=plugin_ffi native_add
 $ cd native_add
Enter fullscreen mode Exit fullscreen mode

Create a GoLang library

The plugin project we created in the previous step will have a src folder. The src folder will have a cpp folder that contains the C++ code. We will create a GoLang library in the src folder. The GoLang library will have a function to add two numbers.

First, we will create a go.mod file. The go.mod file defines the module name and the GoLang version. The go.mod file is used to manage dependencies. We will not use any dependencies in this article.

// go.mod file
module go_code

go 1.21.3
Enter fullscreen mode Exit fullscreen mode

Second, we will create a sum.go file. The sum.go file will have a function to add two numbers. The sum.go file will also have a function called enforce_binding. The enforce_binding function enforces the binding in the ios application. The Dart code will not use the enforce_binding function, but it was the trick I found to make the library linked correctly for iOS.

Above each function we want to expose, we will add a comment //export <function_name>. The //export <function_name> comment exposes the function to other languages. We must also use the type C.int instead of int in the function signature. The C.int type is used to make the function compatible with other languages.

// sum.go file
package main

import "C"

//export sum
func sum(a C.int, b C.int) C.int {
  return a + b
}

//export enforce_binding
func enforce_binding() {}

func main() {}
Enter fullscreen mode Exit fullscreen mode

Compile the GoLang library for Android

Since we are compiling the GoLang library for Android, we must use cross-compilation. Modern Android devices mostly use arm64 or x86_64 architectures. We will compile the GoLang library for both arm64 and x86_64 architectures.

export ANDROID_OUT=../android/src/main/jniLibs
export ANDROID_SDK=$(HOME)/Library/Android/sdk
export NDK_BIN=$(ANDROID_SDK)/ndk/23.1.7779620/toolchains/llvm/prebuilt/darwin-x86_64/bin

# Compile for x86_64 architecture and place the binary file in the android/src/main/jniLibs/x86_64 folder
CGO_ENABLED=1 \
GOOS=android \
GOARCH=amd64 \
CC=$(NDK_BIN)/x86_64-linux-android21-clang \
go build -buildmode=c-shared -o $(ANDROID_OUT)/x86_64/libsum.so .

# Compile for arm64 architecture and place the binary file in the android/src/main/jniLibs/arm64-v8a folder
CGO_ENABLED=1 \
GOOS=android \
GOARCH=arm64 \
CC=$(NDK_BIN)/aarch64-linux-android21-clang \
go build -buildmode=c-shared -o $(ANDROID_OUT)/arm64-v8a/libsum.so .
Enter fullscreen mode Exit fullscreen mode

Now we can use the libsum library in the Dart code using FFI.

DynamicLibrary.open('libsum.so');
Enter fullscreen mode Exit fullscreen mode

Compile the GoLang library for iOS

We need some extra steps to cross-compile the Golang library and use it for iOS. iOS only supports static libraries. So, we need to compile the GoLang library as a static library. We also need to modify the podspec file to use the static library in the Flutter plugin. Another thing to note is that we need to create xcframework for iOS. The xcframework is used to support both simulator and device architectures.

# build_ios.sh file
#!/bin/sh
export GOOS=ios
export CGO_ENABLED=1
# export CGO_CFLAGS="-fembed-bitcode"
# export MIN_VERSION=15

SDK_PATH=$(xcrun --sdk "$SDK" --show-sdk-path)

if [ "$GOARCH" = "amd64" ]; then
    CARCH="x86_64"
elif [ "$GOARCH" = "arm64" ]; then
    CARCH="arm64"
fi

if [ "$SDK" = "iphoneos" ]; then
  export TARGET="$CARCH-apple-ios$MIN_VERSION"
elif [ "$SDK" = "iphonesimulator" ]; then
  export TARGET="$CARCH-apple-ios$MIN_VERSION-simulator"
fi

CLANG=$(xcrun --sdk "$SDK" --find clang)
CC="$CLANG -target $TARGET -isysroot $SDK_PATH $@"
export CC

go build -trimpath -buildmode=c-archive -o ${LIB_NAME}_${GOARCH}_${SDK}.a
Enter fullscreen mode Exit fullscreen mode

The above script will create a static library for iOS. We must use the script to build the static library for both simulator and device architectures.


# Compile for x86_64 simulator architecture
GOARCH=amd64 \
SDK=iphonesimulator \
LIB_NAME=libsum \
./build_ios.sh

# Compile for arm64 simulator architecture
GOARCH=arm64 \
SDK=iphonesimulator \
LIB_NAME=libsum \
./build_ios.sh

# Compile for arm64 device architecture
GOARCH=arm64 \
SDK=iphoneos \
LIB_NAME=libsum \
./build_ios.sh
Enter fullscreen mode Exit fullscreen mode

The above script will create three static libraries. Before creating the xcframework, we must combine the two static libraries for the simulator architecture. The lipo command can combine the two static libraries.

lipo \
-create \
libsum_arm64_iphonesimulator.a \
libsum_amd64_iphonesimulator.a \
-output libsum_iphonesimulator.a
Enter fullscreen mode Exit fullscreen mode

Now we can create the xcframework using the below command.

xcodebuild -create-xcframework \
-output ../ios/libsum.xcframework \
-library ios-arm64/libsum.a \
-headers ios-arm64/libsum.h \
-library ios-simulator/libsum.a \
-headers ios-simulator/libsum.h
Enter fullscreen mode Exit fullscreen mode

The libsum.xcframework will be created in the ios folder.

We need to modify the podspec file to use the xcframework. We must add the lines below to the podspec file under the ios folder.

s.public_header_files = 'Classes/**/*.h'
s.vendored_frameworks = 'libsum.xcframework'
Enter fullscreen mode Exit fullscreen mode

After some trial and error, I found that we need to use the library in the Classes folder to link it properly. So, we create binding.h and EnforceBinding.swift files under the Classes folder.

The binding.h file will have the below code.

void enforce_binding();
Enter fullscreen mode Exit fullscreen mode

The EnforceBinding.swift file will have the below code.

public func dummyMethodToEnforceBundling() {
    enforce_binding() // disable tree shaking
}
Enter fullscreen mode Exit fullscreen mode

Now, we can use the libsum library in the Dart code using FFI.

// The libsum library is statically linked in the native_add so we only need to load native_add.
DynamicLibrary.open('$native_add.framework/native_add');
Enter fullscreen mode Exit fullscreen mode

Create a binding in Dart using ffigen

The ffigen tool generates the Dart code to use the GoLang library. We will create a ffigen.yaml file to configure the ffigen tool. The config.yaml file will have the below code.

# Run with `flutter pub run ffigen --config ffigen.yaml`.
name: NativeLibrary
description: Bindings to `src/sum.h`.
output: 'lib/generated_bindings.dart'
headers:
  entry-points:
    - 'src/libsum.h'
preamble: |
  // ignore_for_file: always_specify_types
  // ignore_for_file: camel_case_types
  // ignore_for_file: non_constant_identifier_names
  // ignore_for_file: unused_field
  // ignore_for_file: unused_element
comments:
  style: any
  length: full
Enter fullscreen mode Exit fullscreen mode

Now, we can run the ffigen tool to generate the Dart code.

flutter pub run ffigen --config ffigen.yaml
Enter fullscreen mode Exit fullscreen mode

The ffigen tool will generate the generated_bindings.dart file under the lib folder based on the libsum.h file in src folder that was generated by the go build command in the previous step.

Use the GoLang library in Dart

Now, we can use the GoLang library in the Dart code. We can use the DynamicLibrary.open method to load the library. We can use the NativeLibrary class to call the functions in the GoLang library.

// lib/native_add.dart
import 'dart:ffi';
import 'dart:io';

import 'generated_bindings.dart';

int sum(int a, int b) => _bindings.sum(a, b);

const String _libName = 'native_add';

/// The dynamic library in which the symbols for [NativeAddBindings] can be found.
final DynamicLibrary _dylib = () {
  if (Platform.isMacOS || Platform.isIOS) {
    return DynamicLibrary.open('$_libName.framework/$_libName');
  }
  if (Platform.isAndroid || Platform.isLinux) {
    return DynamicLibrary.open('libsum.so');
  }
  if (Platform.isWindows) {
    return DynamicLibrary.open('$_libName.dll');
  }
  throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

/// The bindings to the native functions in [_dylib].
final NativeLibrary _bindings = NativeLibrary(_dylib);
Enter fullscreen mode Exit fullscreen mode

Now, we can use the sum function in the Dart code from the Flutter project that uses this plugin.

import 'package:native_add/native_add.dart' as native_add;
native_add.sum(1, 2);
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we checked how to use GoLang in a Flutter application. We created a GoLang library and compiled it for Android and iOS. We also created a Flutter plugin to use the GoLang library in a Flutter application. We used the ffigen tool to generate the Dart code to use the GoLang library. We also checked how to use the GoLang library in the Dart code.

I also created a sample project to demonstrate using GoLang in a Flutter application. You can find the sample project in the below link. The example also contains the script to compile the GoLang library for MacOS.
https://github.com/leehack/flutter_golang_ffi_example
It's still missing for Windows and Linux but it should be much easier than for Android and iOS. Like Android or iOS, we must cross-compile and link the GoLang library for Windows and Linux in the CMake file.

Due to the lack of information, I had to spend a lot of time figuring out how to use GoLang in a Flutter application. This article will help you to use GoLang in a Flutter application.

Top comments (0)