Calling Native Libraries in Flutter with Dart FFI

In this tutorial, you’ll learn how to use Dart FFI to access native libraries that support C-interoperability. By Nick Fisher.

5 (6) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Binding Functions That Return Pointers

The double your C function returns is a stack-allocated primitive that freely passes between C and Dart code without any memory concerns.

What if you want to obtain a Dart String from your C function? The C standard library has no concept of a string, so you can only work with NULL-terminated char array pointers.

Note: If you need a refresher on scope and memory allocation in C, you might want to read these lecture slides.

Proper Scope

You might feel tempted to add the following to weather.c, but don’t:

char* get_forecast() {
  char* forecast = "sunny";
  return forecast;
}

This code will compile unless you’ve turned on the right compiler warnings, but it isn’t valid C!

Why? Because you created a stack-allocated char array, which is only valid within the scope of this function. Once you return the pointer to your Dart code, this no longer points to a valid char array in memory.

To safely return a char pointer, you must return a pointer to properly allocated memory.

Add the following code to weather.c:

char* get_forecast() {
  char* forecast = "sunny";
  char* forecast_m = malloc(strlen(forecast));
  strcpy(forecast_m, forecast);
  return forecast_m;
}

This function creates a local char pointer for the string "sunny", allocates memory for a char pointer memory on the heap of the same size, and copies the contents of the former into the latter.

Then add the following C library includes to the top of weather.c:

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

This code includes header files from the C standard library so that you can use strcpy, strlen and a few other functions and types you’ll see later in this tutorial.

In this tutorial, you know exactly what memory you’re copying, so there’s no need to worry.

Note: In general, you should avoid strcpy and strlen because you can inadvertently introduce security holes by copying untrusted data.

If you’re a seasoned C developer, you already know that returning pointers to locally-scoped variables is a big no-no. At best, it’ll create incorrect return values. At worst, your app will segfault and crash.

If this is your first time working with C, it’s worth reiterating: don’t return pointers to locally-scoped variables!

Next, you need to create a Dart type to represent a function that accepts no arguments with a return value of type Pointer.

Typing Dart Functions That Return Pointers

On the Dart side, create the matching typedefs in ffi_bridge.dart. Locate // TODO: Add new typedef declarations here and replace it with:

typedef ForecastFunction = Pointer<Utf8> Function();
typedef ForecastFunctionDart = Pointer<Utf8> Function();

Dart FFI uses Pointer<Utf8> to represent a char pointer. Keep in mind, the Dart typedef doesn’t return a String because you need to manually free the returned pointer. In contrast, TemperatureFunction directly returns a Dart double.

Add Functions and Their Respective Lookups

Find and replace // TODO: Add _getForecast declaration here with:

ForecastFunctionDart _getForecast;

Here you added a _getForecast of type ForecastFunctionDart to your FFIBridge, which is the Dart function that acts as a bridge to a C function.

Next, replace // TODO: Assign value to _getForecast with:

_getForecast = dl
      .lookupFunction<ForecastFunction, ForecastFunctionDart>('get_forecast');

This uses DynamicLibrary to locate the C function you’ll bridge to, using the name get_forecast and the typedef ForecastFunction.

Find // TODO: Add getForecast() here and replace it with:

  String getForecast() {
    final ptr = _getForecast();
    final forecast = ptr.toDartString();
    calloc.free(ptr);
    return forecast;
  }

In this code, you create a getForecast that invokes the bound Dart function, converts the returned char pointer to a Dart string and frees the memory allocated for the returned pointer.

Return to main.dart and locate // TODO: Add code to invoke newly created forecast method replace it and throw UnimplementedError(); with:

_show(_ffiBridge.getForecast());

This invokes the getForecast() you added in the last step.

Build and run. Then click Today’s forecast.

Screen shot of successful sunny forcast data retrieved.

Voila! Here you:

  1. Obtain a native char pointer.
  2. Convert the pointer to a UTF8/Dart String.
  3. Free the allocated memory.
  4. Pass the String back to your Flutter widget.

Both the get_temperature and get_forecast returned primitive types, a double, and a char pointer respectively. Neither of these functions accepted any arguments.

Next, you’ll see how to invoke a C function that accepts some arguments. You’ll also see how to return a more complicated data structure, not just a simple pointer.

Arguments and Structs

In this section, you’ll see how to pass arguments from Dart to C by creating a function to return a three-day forecast in either Celsius or Fahrenheit.

Creating A Three-Day Forecast Structure

A three-day forecast needs three temperature values, so you obviously can’t return a solitary double. You need to create an appropriate struct.

Add the following to the bottom of weather.c:

struct ThreeDayForecast  {
  double today;
  double tomorrow;
  double day_after;
};

Then add a function that converts between Fahrenheit and Celsius:

double fahrenheit_to_celsius(double temperature) {
  return (5.0f / 9.0f) * (temperature - 32);
}

In the next section, you’ll write the function to create an instance of the ThreeDayForecast struct. This function will then populate the struct’s values with the temperature forecast in either Celsius or Fahrenheit.

Accepting Arguments And Returning Structs

Before your app can provide a three-day forecast in both Fahrenheit and Celsius, you need to create a function that accepts arguments.

At the bottom of weather.c, add:

// 1
struct ThreeDayForecast get_three_day_forecast(bool useCelsius) {
  
  // 2
  struct ThreeDayForecast forecast;
  forecast.today = 87.0f;
  forecast.tomorrow = 88.0f;
  forecast.day_after = 89.0f;
  	
  // 3
  if(useCelsius) {
    forecast.today = fahrenheit_to_celsius(forecast.today);
    forecast.tomorrow = fahrenheit_to_celsius(forecast.tomorrow);
    forecast.day_after = fahrenheit_to_celsius(forecast.day_after);
  }
  // 4
  return forecast;
}

Going through step-by-step, this function:

  1. Accepts a bool indicating whether to return Celsius or Fahrenheit values.
  2. Instantiates a struct with some very boring and static values, representing the forecasted temperature over the next three days.
  3. Converts these values to Celsius if useCelsius is true.
  4. Returns the struct.

Since this function returns a struct, you can’t use the same approach as getForecast() and return a Pointer. You need to create a matching class on the Dart side to receive the values in this struct.