Dartagnan is a POC of C++ Dart bindings generator.
Unlike other generators, Dartagnan preserves OOP semantics when crossing language barrier.
A Dart class can inherit from a C++ class, override virtual methods and still get dynamic dispatch working.
In other words, whether the base class is Dart or C++ is irrelevant from the user's prespective, as inheritance
still works as supposed to.
example/animal.h
#include <iostream>
class Animal {
public:
// non-virtual! for example purposes
void eat() {
std::cout << "eat from C++\n";
openMouth();
closeMouth();
}
virtual void openMouth() {
std::cout << "openMouth from C++\n";
}
virtual void closeMouth() {
std::cout << "closeMouth from C++\n";
}
};
example/app/bin/main.dart
import 'package:AnimalBindings/Bindings.dart' as AnimalBindings;
class MyDartAlligator extends AnimalBindings.Animal
{
@override
void openMouth() {
print("openMouth from Dart\n");
}
}
main() {
final alligator = MyDartAlligator();
/// This will call C++ eat() via FFI
/// which in turn will call into Dart to invoke openMouth()
/// closeMouth() will be C++ to C++ directly, as it's not overridden in Dart
alligator.eat();
// Prints:
// eat from C++
// openMouth from Dart
// closeMouth from C++
}
The example living in example/
already has the bindings pre-generated. You can play with it simply by:
cd example/
cmake -G Ninja .
ninja # produces libAnimal.so
cd app/
dart pub get
cd ..
# Help Dart find libAnimal.so:
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
dart run app/bin/main.dart
On macOS, just edit and hardcode the library path in generated/AnimalBindings/dart/lib/LibraryLoader.dart
, since DYLD_LIBRARY_PATH
isn't recommended nowadays.l
animal.h and animal.cpp
The C++ source code we generated bindings for.animal_export.h
Export macros, generated by CMake, since we need to export symbols.bindings_global.h
Contains the list of headers we generate bindings for.dartagnan.json
Config read by dartagnan when generating the bindingstypesystem.xml
Tells dartagnan which classes and enums to generate bindings forapp/
The dart application which will load and use the bindingsgenerated/AnimalBindings/dart/
Auto-generated Dart package containing the bindingsgenerated/AnimalBindings/dart/ffi/
Auto-generated C++ code which is needed as an intermediary, to implement virtual dispatch.
For convenience, our libAnimal.so
's CMakeLists.txt builds both animal.cpp
and the autogenerated Animal_c.cpp
. You could also have them separated
libAnimal.so
and libAnimalBindings.so
, which would be better in case you're targeting a 3rdparty.
git submodule init && git submodule update
cmake --preset=dev
cd build-dev && ninja
Generating your own bindings consists basically of writing your own typesystem.xml
file specifing what you want to generate bindings for.
Then make an invocation such as:
../build-dev/3rdparty/shiboken/sources/shiboken6/generator/shiboken6 \
--license-file=license_template \
--output-directory=generated \
--generator-set=dart --skip-deprecated --clang-option=-DDARTAGNAN_BINDINGS_RUN bindings_global.h typesystem.xml
clang-format -i ./generated/AnimalBindings/dart/ffi/Animal_c.cpp ./generated/AnimalBindings/dart/ffi/Animal_c.h
Dartagnan is based on shiboken, which is use to generated Python bindings for Qt.
- Single inheritance
- virtual methods
- static methods
- Some templates
- const char * <-> dart String is interchangeable
- Blacklisting methods via typesystem.xml (Shiboken feature)
- C++ destructors are called! calling
myCFunction(MyCType(10, 20))
creates a temporaryMyCType()
as expected and destroys it at the end of expression. This is done with Dart Finalizers.
-
Templates are a PITA. The C++ needs to contain the specializations. You can't specialize from Dart if it wasn't beforehand done in C++.
-
Dart doesn't support more than one unnamed constructor. The generated bindings look like
MyClass.ctor1()
,MyClass.ctor2()
etc. -
Actually, same for methods. Dart doesn't support overloading. Generated code is prefixed with numbers.
-
Passing small and trivial-destructible structs to C++ is allocating memory. They should be passed via registers, or at the very least stack.
-
C++ is complex, Dartagnan often breaks and needs patches with complex projects.
-
The POC code is a mess, specially after shoehorning C++ template support.
-
Dartagnan is based on shiboken's Api-extractor, which is a LLVM-based C++ parser. Api extractor brings in Qt as a dependency.
-
Rebasing over new shiboken, newer LLVM and newer Qt often causes regressions.
While Dartagnan works and is even being used in production for KDDockWidgets we don't have any plans to move it from POC to something serious.
The shiboken legacy is quite a burden and its usage is very involved.
If someone wants to write a new backend from scratch I hope Dartagnan is a useful inspiration.