We are friends of Flutter with C# and C++

Flutter is very convenient and well written interfaces for users. But using Dart to solve algorithmic problems is cumbersome and inefficient. The C family does a much better job and makes it easy to parallelize calculations. In addition, over the years, C ++ and C # have acquired many useful libraries, not all of which have analogues in Flutter.

Knowing about the existence of the FFI library for Flutter, which even allows you to run C code synchronously, I decided to dig into this topic and try to combine the legacy of C and their efficiency with a convenient framework. Considering that there is not enough information on the Internet about the use of FFI, especially with C #, I decided to share my experience of “building bridges” using the example of two applications in this article.

A bit of theory

FFI is a foreign function interface, or an interface of external functions. It is a mechanism that allows a program in one language to call functions written in another. In the case of Flutter, this is the ability to call C functions from compiled libraries from Dart. Functions that can be called from outside must be marked as external before compilation and be static.

The FFI library allows you to work with external functions on all popular platforms. But in this article, I checked the library only for Windows, however, I don’t think that there should be out of the ordinary complications on other platforms.

Working with Simulation in C++

My first term paper was written in pluses and output results as bmp files. When the time came for the second term paper, I realized that I clearly needed to make at least some kind of interface other than Windows Explorer. Rewriting thousands of lines of code is such a pleasure. In addition, Dart is still a single-threaded language, and it will not be possible to parallelize calculations as easily as with openMP. So it was decided to create a program in this way: C ++ executes the simulation steps, stores all the data and state in itself, and Flutter supports the simulation and receives the simulation state, displaying it on the interface.

Thankfully, there documentation on how to create an ffi plugin. You can easily find the command in it:

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

The created plugin even immediately has an example that you can study to understand how the function call works from the point of view of the code.

However, here the first problem awaited me – the Dart code in this template is written using ffigen. Code generation is always good, but it was painful to realize that it only works for pure C. So I had to record each function call myself, as well as edit the generated CMake file.

The latter is easy enough. It is necessary to enter all the necessary files with code (for example, main.cpp can be omitted), specify the header file, and also include the necessary packages (for example, openMP, so that its directives work). Here example from my project.

Before you start working with functions, you need to configure the connection to the library itself. To do this, just write the path to it and dynamically open it with DynamicLibrary.open(pathToLib/libName.dll). It is from this object using the methodlookup we will call functions by their name on the C++ side.

But even this is not enough, because only simple data types can be transferred between languages. That is, the function arguments and return type in both languages ​​must be simple. To pass structured data, you need to use pointers and structures. In Dart, you need to create a class that inherits from Structand also make its analogue in C++ code, and then work with this class through the Dart class Pointer. Moreover, it is imperative to preserve the order of the fields of structures in both languages:

Structure example in Dart:

final class IntArray extends Struct{
  external Pointer<Int32> data;
  
  @Int32()
  external int length;
}

An example of the same structure in C++:

typedef struct {
  int *data;
  int length;
} IntArray;

Here is an example of creating array. Other structures are created similarly. As an example, the model structure locations, which is used in the simulation. Since I would not like to work with the Pointer class in the business logic of the application, we write a class without pointers for each structure.

It is worth mentioning that pointer creation in Dart is much more problematic than in C++. To create, you need to connect not only the library dart:ffibut also the package package:ffi/ffi.dartin order to use the class Arenawith which you can allocate memory:

  1. Create an object to allocate memory
    Arena arena = Arena();

  2. We create the necessary pointers, for example, in this way:
    Pointer<Int> townsPriority = arena.allocate(length * sizeInt);

  3. After the created pointers are no longer needed, we clear the memory:
    arena.releaseAll();

Now that we have all the necessary data structures and a library connection, it’s time to write global external functions and functions that call external functions.
On the C++ side, I created a separate file FlutterAdapter.cpp, which stores everything for working with external functions. Of the unusual in it, onlyFFI_PLUGIN_EXPORTbefore those functions that are external. Similarly FFI_PLUGIN_EXPORT you need to write in the header file before declaring the functions. In addition, it still needs to do two things. First, add to the beginning:

#if _WIN32
#define FFI_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FFI_PLUGIN_EXPORT
#endif

And second, wrap all declarations in extern "C" {}. This keyword is necessary to preserve the code when translating C++ and C. However, adding it may cause problems due to, for example, the lack of support templateat the functions.

I agree with the idea that it’s easier to see once than to try to read the theory, so for the Dart side, I’ll just show examples: here example code with getting a pointer, and here example code with argument passing in C++.

But that’s not all! Since Dart is single-threaded, when you call a long-running function on the C ++ side, the interface on Flutter will die and hang. To prevent this from happening, you need to set up work with a separate isolate. This is quite a lot of complex code that needed to be repeated for each such function (and ffigen had to do this work), but I managed to set up an isolate call for any function using a couple of crutches. Such function calls look like this:

Future<void> makeStep(int stepCount) async {
  return await executeInIsolate<void>(
    _makeStepForIsolate, {'stepCount': stepCount});
}
void _makeStepForIsolate(Map<String, dynamic> args) {
  final execute = lookup<NativeFunction<Void Function(Int)>>('execute')
    .asFunction<void Function(int)>();
  return execute(args['stepCount']);
}

Everything looks so crutch because in SendPort only top-level functions can be passed. For this, it is separately declared _makeStepForIsolate.Arguments are written to the Map in order to preserve the ability to pass absolutely any function inside executeInIsolate. We are waiting for macros in Dart.
For those who want to understand how isolate is created and functions are executed in a separate thread, here is this I left a lot of comments in the file.

Before summing up, I want to say that it was quite problematic to write this project because of problems with debugging. I wrote the Flutter code in VScode and sometimes made edits to the plus code through it. Firstly, when compiling and running the program, C++ errors are displayed in the debug console, but in a different encoding, which is why most of the errors I had to google by its number, because the text of the error was a bunch of questions. Most likely it is because of the Russian language. CMake settings in vs-code didn’t help. Secondly, I noticed a problem that when creating a function with 20 or more arguments, the program ends without an error.

Summarize. To build bridges between languages, you need to: configure CMake, mark external functions on the C++ side and compile the code to dll, connect to this library on Dart and call the function by name, if necessary, create identical structures in two languages ​​and pass pointers to them, and when the function is heavy, we create a separate isolate for the call.

Workflow in C#

There was a task for the desktop: to optimize the filling of certain types of documents so that they could be done not through Office, but through a convenient special program. Of course, I immediately remembered C #, because a couple of times I already wrote something similar in it. But the desire to experiment with FFI made me reconsider my choice towards two languages ​​at once. And, besides, at the time of creating the program for Flutter there was no normal library for working with Word, and the customer wanted to be able to edit both Word and Excel.

Having suffered with the previous project, it was decided to minimize the points of contact between the two languages ​​in this one. As a consequence of this, the program has only two external functions, the arguments of which are jsons. When calling a method makeFile in one large json, information about all the necessary structures is transmitted. It is very convenient, although, of course, you need to understand that this interferes with performance a little. In addition, so that there are no problems with the format, you must definitely usejson.encodeon the side of Dart and not get confused in encodings. Let me remind you that Utf8 does not support Cyrillic. So the preparation of arguments on the Dart side looks like this:

final json = json.encode(data.toJson());
final pointer = json.toNativeUtf16();
final result = dartFunc(pointer);

So this time, absolutely all the work with FFI on the Dart side fit into one file.

If last time a simple CMake file was immediately created, in which it was necessary to fill in a couple of lines so that everything worked at the click of a button, this time I had only minimal information from the Internet. Unfortunately, my search turned up nothing. However, this is not really necessary, because all the compilation work can be done using a regular script.

Regarding the creation of the dll file itself. .NET Core supports AOT compilation, just like Dart, but only with a specific compiler and a couple of dotnet commands. All steps were done on .net 6.0. Can’t tell for sure if the commands work on a higher version. I would be glad if someone writes in the comments whether it turned out at a higher one.

To get started, I created a regular C# console application through VS. Next, through the console, add the necessary compiler with the command:

dotnet add package Microsoft.DotNet.ILCompiler -v 7.0.0-*

Now the project is ready to be filled with code and libraries. In this program, I used epplus to work with excel, which I connected via nuget. External functions are also separated into a separate filemake it static and mark it with an attribute that stores the name that we will look for through lookup:

[UnmanagedCallersOnly(EntryPoint = "makeFile")]

We use Marshal.PtrToStringUni, keeping encoding in mind and using built-in deserialization to get models ready to go. Just because of the json transfer format, there was a problem with the naming of the fields of the transferred models, since the styles in Dart and C # are different. I decided that let C# tolerate this violation, so I made the field names exactly like in Dart, that is, with a small letter:

public string name { get; set; }

We test the program in the console application mode, but for compilation, we must change the project type to a class library. To get the dll file, build the project and write to the console:

dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r win-x64 -c release(или debug)

After quite a long time of the compiler’s work, a dll file appeared in the bin/Release/net.6.0/win-x64/publish folder. Inside it, all the libraries he needs are immediately included, so it’s enough to use only him.

Summarize. This time, all you need to do is create a project and connect the compiler, mark external functions, compile the dll and move it to the right place, connect to the library in Dart and pass json to the function call.

Conclusion

As a result, I got two applications, with different algorithmic languages ​​and with different approaches to building a connection with Flutter. In general, the knowledge gained in practice is quite enough to include the C family code into the project without fear next time.

I consider the increase in productivity and the expansion of the pool of libraries to be the advantages of this approach, because now, in addition to pub, we have all the C ++ and C # libraries.
The disadvantages include the increased weight of the application, the increase in the compilation time of the project and the complexity of debugging.

I will be glad if my experience will be useful to someone useful.
I am attaching links to projects that I used as examples:
Flutter&C++: https://github.com/iamgirya/Physarum-building-an-optimal-road-network
Flutter&C#: https://github.com/NullExp-Team/builders_act_maker

And a few sources that helped me as I developed the application
https://medium.com/@stevehamblett/using-c-libraries-in-dart-ec630848d52c – C# example
https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish – about dotnet publish
https://www.programmersought.com/article/589910582570/ – structure guide

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *