cd ../

Calling Golang from Flutter Using FFI (Foreign Function Interface)

sabitur_rahman··2 min read

Do you want to call Go code from a Dart or Flutter project? In this tutorial, I’ll show you how to use FFI (Foreign Function Interface) to send data from Dart to Go and get a response back — using a notification example!

If you already know about Go then you can say why do we use import "C". let’s break down

Go doesn’t speak dart(or python or rust) but it speak C

When Dart uses FFI to call native functions, it expects to talk to a C-compatible shared library — one that uses the standard C calling conventions.

Go doesn’t natively produce functions that can be called directly by Dart or other languages. However, it can produce a C-compatible interface — and that’s exactly what import "C" does.

We convert C strings to Go using C.GoString.


package main

import "C"

import (
    "github.com/ciarand/notify"
)

//export ShowNotification
func ShowNotification(title *C.char, message *C.char) *C.char {
    goTitle := C.GoString(title)
    goMessage := C.GoString(message)

    n := notify.NewNotification(goTitle, goMessage)

    err := n.Display()
    if err != nil {
        return C.CString("Notification failed: " + err.Error())
    }
    return C.CString("Notification sent successfully")
}

func main() {}

After that, compile it in various formats based on the platform, such as .dylib for macOS and .so for Linux.

cmd:

go build -buildmode=c-shared -o libnotify.dylib notifylib.go
go build -buildmode=c-shared -o libnotify.so notifylib.go

In Dart:

create dart project and put Go library in bin folder

dart_ffi/
├── bin/
│   ├── main.dart
│   └── libnotify.dylib

Dart Memory ≠ Native Memory

Dart manages its own memory (like JavaScript or Python), and native code (Go, C, Rust) manages memory separately. Dart strings (String) are not directly compatible with native code.

To pass a Dart string to native code, you must:

  1. Convert the Dart string to a C-style string using .toNativeUtf8()

  2. Manually allocate memory for it in the native heap using malloc

  3. Pass that memory pointer to native code

If you don’t call malloc.free() after the native call finishes, the memory you allocated will remain unused — but still reserved. This is called a memory leak.

main.dart


import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';

void main(List<String> arguments) {
  callNotification("Hello from Flutter", "This is sent via Go FFI!");
}

void callNotification(String title, String message) {
  final titlePtr = title.toNativeUtf8();
  final messagePtr = message.toNativeUtf8();

  final resultPtr = showNotification(titlePtr, messagePtr);
  final result = resultPtr.toDartString();

  print("Result from Go: $result");

  malloc.free(titlePtr);
  malloc.free(messagePtr);
}

final DynamicLibrary nativeLib = Platform.isWindows
    ? DynamicLibrary.open("bin\\libnotify.dll")
    : Platform.isMacOS
        ? DynamicLibrary.open("bin/libnotify.dylib")
        : DynamicLibrary.open("bin/libnotify.so"); // For Linux

// FFI typedefs
typedef ShowNotificationNative = Pointer<Utf8> Function(
  Pointer<Utf8>, Pointer<Utf8>);

typedef ShowNotificationDart = Pointer<Utf8> Function(
  Pointer<Utf8>, Pointer<Utf8>);

final ShowNotificationDart showNotification = nativeLib
    .lookup<NativeFunction<ShowNotificationNative>>("ShowNotification")
    .asFunction();

Output: