dart-lang / dart-lang/native

I am running into an `Undefined symbol` issue with dart-ffi and Go

Open
#1,356 4 comments 0 reactions 0 assignees View on GitHub
package:ffigen
Dominant language
Dart
Stars
275
Forks
144
Avg merge
2d 10h
Merged PRs (30d)
47

Description

I am trying to use [ffigen](https://pub.dev/packages/ffigen) to develop a library that I can use to call a go package from dart code.

My current approach is to use the basic setup that you get from running the initial ffigen

`flutter create --template=plugin_ffi --platforms=android,ios,linux,macos,windows project_name`

I am compiling my go code(CGO) to generate a header file that I then pull into the basic header file provided by running the initial ffigen creation script and use in the C code from the initial basic setup.

Whenever I do this, I get the error Error (Xcode): Undefined symbol: _parse_data

Here is my Go code that generates the header file:
```
//main.go

package main

/*
#include
*/
import "C"
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strings"

"github.com/erh/gonmea/analyzer"
)

//export parse_data
func parse_data(byteArr []byte) *C.char {
fmt.Println(string(byteArr))
in := io.NopCloser(bytes.NewReader(byteArr))
defer in.Close()

parser, err := analyzer.NewParser()
if err != nil {

return C.CString(fmt.Sprintf("Error: %v", err))
}

reader := bufio.NewReader(in)
for {
line, _, err := reader.ReadLine()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return C.CString(fmt.Sprintf("Error: %v", err))
}
line = []byte(strings.TrimSpace(string(line)))
if len(line) == 0 {
continue
}
msg, err := parser.ParseMessage(line)
if err != nil {
return C.CString(fmt.Sprintf("Error: %v", err))
}
md, err := json.MarshalIndent(msg, "", " ")
if err != nil {
return C.CString(fmt.Sprintf("Error: %v", err))
}
fmt.Println(string(md))
return C.CString(string(md))
}
}

func main() {}

```

Here is the header file generated by the go code

```

/* Code generated by cmd/cgo; DO NOT EDIT. */

/* package command-line-arguments */

#line 1 "cgo-builtin-export-prolog"

#include

#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H

#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
#endif

#endif

/* Start of preamble from import "C" comments. */

#line 3 "main.go"

#include

#line 1 "cgo-generated-wrapper"

/* 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 GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef size_t GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
#ifdef _MSC_VER
#include
typedef _Fcomplex GoComplex64;
typedef _Dcomplex GoComplex128;
#else
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
#endif

/*
static assertion to make sure the file is being used on architecture
at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];

#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif
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* parse_data(GoSlice byteArr);

#ifdef __cplusplus
}
#endif

```

Here is the header file generated from running the initial ffigen creation script

```
#ifndef GONMEA_FLUTTER_H
#define GONMEA_FLUTTER_H

#include
#include
#include
#include "libgonmea_flutter.h"

#if _WIN32
#include
#else
#include
#include
#endif

#if _WIN32
#define FFI_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FFI_PLUGIN_EXPORT __attribute__((visibility("default"))) __attribute__((used))
#endif

// A very short-lived native function.
FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b);

extern char* parse_data(GoSlice byteArr);

FFI_PLUGIN_EXPORT char* parse_data_wrapper(GoSlice byteArr);

#endif
````
Here is the C code

```
#include "gonmea_flutter.h"

FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b) {
return a + b;
}

FFI_PLUGIN_EXPORT char* parse_data_wrapper(GoSlice byteArr) {
return parse_data(byteArr);
}
```

Here is my ffigen.yaml file

```
# Run with `flutter pub run ffigen --config ffigen.yaml`.
name: GonmeaFlutterBindings
description: |
Bindings for `src/gonmea_flutter.h`.

Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
output: 'lib/gonmea_flutter_bindings_generated.dart'
headers:
entry-points:
- 'src/gonmea_flutter.h'
include-directives:
- 'src/gonmea_flutter.h'
- 'src/libgonmea_flutter.h'
preamble: |
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
comments:
style: any
length: full
```
Here is the autogenerated dart-ffi bindings generated by running flutter pub run ffigen --config ffigen.yaml

```
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names

// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
// ignore_for_file: type=lint
import 'dart:ffi' as ffi;

/// Bindings for `src/gonmea_flutter.h`.
///
/// Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
///
class GonmeaFlutterBindings {
/// Holds the symbol lookup function.
final ffi.Pointer Function(String symbolName)
_lookup;

/// The symbols are looked up in [dynamicLibrary].
GonmeaFlutterBindings(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;

/// The symbols are looked up with [lookup].
GonmeaFlutterBindings.fromLookup(
ffi.Pointer Function(String symbolName)
lookup)
: _lookup = lookup;

ffi.Pointer parse_data(
GoSlice byteArr,
) {
return _parse_data(
byteArr,
);
}

late final _parse_dataPtr =
_lookup Function(GoSlice)>>(
'parse_data');
late final _parse_data =
_parse_dataPtr.asFunction Function(GoSlice)>();

/// A very short-lived native function.
int sum(
int a,
int b,
) {
return _sum(
a,
b,
);
}

late final _sumPtr =
_lookup>(
'sum');
late final _sum = _sumPtr.asFunction();

ffi.Pointer parse_data_wrapper(
GoSlice byteArr,
) {
return _parse_data_wrapper(
byteArr,
);
}

late final _parse_data_wrapperPtr =
_lookup Function(GoSlice)>>(
'parse_data_wrapper');
late final _parse_data_wrapper = _parse_data_wrapperPtr
.asFunction Function(GoSlice)>();
}

final class _GoString_ extends ffi.Struct {
external ffi.Pointer p;

@ptrdiff_t()
external int n;
}

typedef ptrdiff_t = __darwin_ptrdiff_t;
typedef __darwin_ptrdiff_t = ffi.Long;

final class GoInterface extends ffi.Struct {
external ffi.Pointer t;

external ffi.Pointer v;
}

final class GoSlice extends ffi.Struct {
external ffi.Pointer data;

@GoInt()
external int len;

@GoInt()
external int cap;
}

typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong;

```
Finally, here is my implementation where I called the auto generated ffi bindings

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

import 'package:flutter/foundation.dart';

import 'gonmea_flutter_bindings_generated.dart';

/// A very short-lived native function.
///
/// For very short-lived functions, it is fine to call them on the main isolate.
/// They will block the Dart execution while running the native function, so
/// only do this for native functions which are guaranteed to be short-lived.
int sum(int a, int b) => _bindings.sum(a, b);

String? _processDataHelper(Uint8List data) {
final rawDataPtr = calloc(data.length);
final rawDataList = rawDataPtr.asTypedList(data.length);
rawDataList.setAll(0, data);

final goSlicePtr = calloc();
goSlicePtr.ref.data = rawDataPtr.cast();
goSlicePtr.ref.len = data.length;
goSlicePtr.ref.cap = data.length;

final resultPtr = _bindings.parse_data_wrapper(goSlicePtr.ref);

String? result;
if (resultPtr != nullptr) {
result = resultPtr.cast().toDartString();
calloc.free(resultPtr.cast());
}

// Free remaining allocated memory
calloc.free(goSlicePtr);
calloc.free(rawDataPtr);

return result;
}

Map _stringToMap(String dartString) {
try {
final jsonString = json.decode(dartString);
return jsonString;
} catch (e) {
return {};
}
}

Map processData(Uint8List data) {
final readingsString = _processDataHelper(data);
if (readingsString != null) {
return _stringToMap(readingsString);
}
return {};
}

const String _libName = 'gonmea_flutter';

/// The dynamic library in which the symbols for [GonmeaFlutterBindings] 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('lib$_libName.so');
}
if (Platform.isWindows) {
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

/// The bindings to the native functions in [_dylib].
final GonmeaFlutterBindings _bindings = GonmeaFlutterBindings(_dylib);
```

Please let me know if there's anything missing 🙏🏻

I have tried to directly use the generated header file in the C code and remove the autogenerated code and that doesn't seem to fix it

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.