diff --git a/pkgs/ffigen/lib/src/code_generator/cpp_class.dart b/pkgs/ffigen/lib/src/code_generator/cpp_class.dart index a2e6cb13d8..d159b3dcd7 100644 --- a/pkgs/ffigen/lib/src/code_generator/cpp_class.dart +++ b/pkgs/ffigen/lib/src/code_generator/cpp_class.dart @@ -194,6 +194,17 @@ class $name implements $ffiPrefix.Finalizable { _activeFinalizerFn = null; } + /// Detaches the finalizer and invalidates this object, returning the + /// underlying C++ pointer. + /// + /// Throws a [StateError] if the object has already been disposed, or if + /// this object does not own the pointer. + $ptrVoid detachPointer() { + final rawPtr = _ptr; + releaseOwnership(); + _ptr = $ffiPrefix.nullptr; + return rawPtr; + } '''); for (final ctor in constructors) { @@ -201,8 +212,8 @@ class $name implements $ffiPrefix.Finalizable { final privateName = '_$glueName'; final dartParams = dartParamList(ctor.parameters); - final localVars = LocalVariables(ctor.localScope); + final callArgs = ctor.parameters .map( (p) => p.type.convertDartTypeToFfiDartType( @@ -227,8 +238,8 @@ class $name implements $ffiPrefix.Finalizable { final glue = '_${method.name.name}'; final dartReturn = method.returnType.getDartType(ctx); final dartParams = dartParamList(method.parameters); - final localVars = LocalVariables(method.localScope); + final callArgs = [ if (!method.isStatic) '_ptr', ...method.parameters.map( @@ -249,11 +260,14 @@ class $name implements $ffiPrefix.Finalizable { objCRetain: false, ); + final hasReturn = method.returnType != voidType; + final callLine = hasReturn ? 'return $returnExpr;' : '$returnExpr;'; + if (method.isStatic) { s.write('''\ static $dartReturn ${method.originalName}($dartParams) { $decls - return $returnExpr; + $callLine } '''); } else { @@ -263,7 +277,7 @@ class $name implements $ffiPrefix.Finalizable { throw StateError('This object has already been disposed.'); } $decls - return $returnExpr; + $callLine } '''); } @@ -365,12 +379,13 @@ FFIGEN_EXPORT void ${name}_delete($originalName* self) { final methodBindings = methods .map((method) { final symbol = method.name.name; - final callArgs = method.parameters.map((p) => p.name).join(', '); final String returnTypeString; final String params; final String body; + final callArgs = method.parameters.map(_cppCallArg).join(', '); + if (method.isConstructor) { returnTypeString = '$originalName*'; params = method.parameters.map(paramDecl).join(', '); @@ -396,7 +411,11 @@ FFIGEN_EXPORT void ${name}_delete($originalName* self) { selfType = originalName; } params = ['$selfType* self', ...otherParams].join(', '); - body = '${returnPrefix}self->${method.originalName}($callArgs);'; + final methodName = method.originalName; + final suffix = method.returnType is CppUniquePtrType + ? '.release()' + : ''; + body = '${returnPrefix}self->$methodName($callArgs)$suffix;'; } } @@ -436,3 +455,12 @@ FFIGEN_EXPORT $returnTypeString $symbol($params) { visitor.visit(ffiImport); } } + +String _cppCallArg(Parameter p) { + final type = p.type; + if (type is CppUniquePtrType) { + final className = type.cppClass.originalName; + return 'std::unique_ptr<$className>(${p.name})'; + } + return p.name; +} diff --git a/pkgs/ffigen/lib/src/code_generator/pointer.dart b/pkgs/ffigen/lib/src/code_generator/pointer.dart index 3f1e99794d..6e2a5ecf9b 100644 --- a/pkgs/ffigen/lib/src/code_generator/pointer.dart +++ b/pkgs/ffigen/lib/src/code_generator/pointer.dart @@ -255,8 +255,9 @@ class ObjCObjectPointerWithProtocols extends ObjCObjectPointer { } /// A pointer to a C++ class wrapper object. -/// Returned pointers are always unowned by default. The developer must call -/// `retainOwnership()` explicitly if ownership has been transferred. +/// +/// Returned pointers are unowned by default. The developer must call +/// `retainOwnership()` explicitly if ownership has been transferred. class CppClassPointerType extends PointerType { final CppClass cppClass; @@ -314,3 +315,31 @@ class CppClassPointerType extends PointerType { visitor.visit(ffiImport); } } + +/// A type representing `std::unique_ptr` ownership transfer. +class CppUniquePtrType extends CppClassPointerType { + CppUniquePtrType(super.cppClass); + + @override + String convertDartTypeToFfiDartType( + Context context, + String value, { + required bool objCRetain, + required bool objCAutorelease, + required LocalVariables localVariables, + }) => '$value.detachPointer()'; + + @override + String convertFfiDartTypeToDartType( + Context context, + String value, { + required bool objCRetain, + String? objCEnclosingClass, + }) => '${cppClass.name}.fromPointer($value, takeOwnership: true)'; + + @override + String toString() => 'unique_ptr<${cppClass.name}>'; + + @override + String cacheKey() => 'unique_ptr<${cppClass.cacheKey()}>'; +} diff --git a/pkgs/ffigen/lib/src/code_generator/writer.dart b/pkgs/ffigen/lib/src/code_generator/writer.dart index 6a957e6fb6..1ed0185be9 100644 --- a/pkgs/ffigen/lib/src/code_generator/writer.dart +++ b/pkgs/ffigen/lib/src/code_generator/writer.dart @@ -419,6 +419,7 @@ id objc_retainBlock(id); String? generateCpp(String outFilename) { final s = StringBuffer(); final outDir = p.dirname(outFilename); + s.write('#include \n'); // Emit each entry-point header exactly once. for (final header in context.config.headers.entryPoints) { s.write('#include "${p.relative(header.toFilePath(), from: outDir)}"\n'); diff --git a/pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart b/pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart index 6eb46d648c..c9d931d0a6 100644 --- a/pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart +++ b/pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart @@ -470,6 +470,19 @@ class Clang { late final _clang_Type_getNumObjCProtocolRefs = _clang_Type_getNumObjCProtocolRefsPtr.asFunction(); + /// Returns the number of template arguments for given template + /// specialization, or -1 if type \c T is not a template specialization. + int clang_Type_getNumTemplateArguments(CXType T) { + return _clang_Type_getNumTemplateArguments(T); + } + + late final _clang_Type_getNumTemplateArgumentsPtr = + _lookup>( + 'clang_Type_getNumTemplateArguments', + ); + late final _clang_Type_getNumTemplateArguments = + _clang_Type_getNumTemplateArgumentsPtr.asFunction(); + /// Retrieves the base type of the ObjCObjectType. /// /// If the type is not an ObjC object, an invalid type is returned. @@ -501,6 +514,23 @@ class Clang { _clang_Type_getObjCProtocolDeclPtr .asFunction(); + /// Returns the type template argument of a template class specialization + /// at given index. + /// + /// This function only returns template type arguments and does not handle + /// template template arguments or variadic packs. + CXType clang_Type_getTemplateArgumentAsType(CXType T, int i) { + return _clang_Type_getTemplateArgumentAsType(T, i); + } + + late final _clang_Type_getTemplateArgumentAsTypePtr = + _lookup>( + 'clang_Type_getTemplateArgumentAsType', + ); + late final _clang_Type_getTemplateArgumentAsType = + _clang_Type_getTemplateArgumentAsTypePtr + .asFunction(); + /// Provides a shared context for creating translation units. /// /// It provides two options: diff --git a/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart b/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart index 60762f7516..7f8ce870f7 100644 --- a/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart +++ b/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart @@ -43,6 +43,25 @@ Type getCodeGenType( return getCodeGenType(context, clang.clang_Type_getNamedType(cxtype)); } + // Handle C++ templates like std::unique_ptr. + if (context.config.cpp?.classes != null) { + final numTemplateArgs = clang.clang_Type_getNumTemplateArguments(cxtype); + if (numTemplateArgs >= 1) { + final declCursor = clang.clang_getTypeDeclaration(cxtype); + final usr = declCursor.usr(); + final isStdUniquePtr = usr.startsWith('c:@N@std@S@unique_ptr'); + if (isStdUniquePtr) { + final spelling = cxtype.spelling(); + return _extractUniquePtrType( + context, + cxtype, + numTemplateArgs, + spelling, + ); + } + } + } + // These basic Objective C types skip the cache, and are conditional on the // language flag. if (context.config.objectiveC != null) { @@ -287,6 +306,41 @@ Type? _extractfromRecord( return UnimplementedType('${cxtype.kindSpelling()} not implemented'); } +Type _extractUniquePtrType( + Context context, + clang_types.CXType cxtype, + int numTemplateArgs, + String spelling, +) { + final logger = context.logger; + + if (numTemplateArgs != 1) { + logger.warning( + 'std::unique_ptr with a custom deleter is not supported ' + '($numTemplateArgs template args in "$spelling"). Skipping.', + ); + return UnimplementedType('unique_ptr with custom deleter not supported'); + } + + final innerCXType = clang.clang_Type_getTemplateArgumentAsType(cxtype, 0); + final innerType = getCodeGenType(context, innerCXType); + + if (innerType is CppClass) { + logger.fine( + ' unique_ptr<${innerType.originalName}> is an owned CppUniquePtrType', + ); + return CppUniquePtrType(innerType); + } + + logger.warning( + 'std::unique_ptr inner type is not a known C++ class ' + '(got ${innerType.runtimeType} from "$spelling"). Skipping.', + ); + return UnimplementedType( + 'unique_ptr inner type is not a supported C++ class', + ); +} + // Used for function pointer arguments. Type _extractFromFunctionProto( Context context, diff --git a/pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart b/pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart index 4f80f0da4b..f4eab46cd6 100644 --- a/pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart +++ b/pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart @@ -89,6 +89,18 @@ class Animal implements ffi.Finalizable { _activeFinalizerFn = null; } + /// Detaches the finalizer and invalidates this object, returning the + /// underlying C++ pointer. + /// + /// Throws a [StateError] if the object has already been disposed, or if + /// this object does not own the pointer. + ffi.Pointer detachPointer() { + final rawPtr = _ptr; + releaseOwnership(); + _ptr = ffi.nullptr; + return rawPtr; + } + factory Animal(int age) { return Animal.fromPointer(_Animal_new(age), takeOwnership: true); } @@ -97,7 +109,7 @@ class Animal implements ffi.Finalizable { throw StateError('This object has already been disposed.'); } - return _Animal_speak(_ptr); + _Animal_speak(_ptr); } int getAge() { @@ -113,11 +125,11 @@ class Animal implements ffi.Finalizable { } static void Animal_new() { - return _Animal_Animal_new(); + _Animal_Animal_new(); } static void Animal_delete() { - return _Animal_Animal_delete(); + _Animal_Animal_delete(); } bool isMammalClass() { @@ -296,6 +308,18 @@ class FinalizerTestSubject implements ffi.Finalizable { _activeFinalizerFn = null; } + /// Detaches the finalizer and invalidates this object, returning the + /// underlying C++ pointer. + /// + /// Throws a [StateError] if the object has already been disposed, or if + /// this object does not own the pointer. + ffi.Pointer detachPointer() { + final rawPtr = _ptr; + releaseOwnership(); + _ptr = ffi.nullptr; + return rawPtr; + } + factory FinalizerTestSubject(ffi.Pointer counter) { return FinalizerTestSubject.fromPointer( _FinalizerTestSubject_new(counter), diff --git a/pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart.cpp b/pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart.cpp index aa786c58fb..a303235874 100644 --- a/pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart.cpp +++ b/pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart.cpp @@ -1,3 +1,4 @@ +#include #include "cpp_class_test.h" #include "finalizer_test_subject.h" diff --git a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases.cpp b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases.cpp index aaf360e34e..e199a6cadc 100644 --- a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases.cpp +++ b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases.cpp @@ -48,3 +48,19 @@ int NodeManager::takeNode(Node* node) { delete node; return val; } + +std::unique_ptr NodeManager::makeNode(int value, int* destructorCounter) { + return std::make_unique(value, destructorCounter); +} + +int NodeManager::consumeNode(std::unique_ptr node) { + return node->getValue(); +} + +NodeContainer::NodeContainer(std::unique_ptr node) + : node_(std::move(node)) {} + +int NodeContainer::getValue() const { + return node_ ? node_->getValue() : -1; +} + diff --git a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases.h b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases.h index 74cc13456e..06b6dc2a20 100644 --- a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases.h +++ b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases.h @@ -2,6 +2,9 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +#include + + class Node { public: Node(int value, int* destructorCounter); @@ -23,5 +26,15 @@ class NodeManager { Node* getSingletonNode(int value, int* destructorCounter); int getValue(Node* node); int takeNode(Node* node); + std::unique_ptr makeNode(int value, int* destructorCounter); + int consumeNode(std::unique_ptr node); +}; + +class NodeContainer { +public: + NodeContainer(std::unique_ptr node); + int getValue() const; +private: + std::unique_ptr node_; }; diff --git a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_bindings.dart b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_bindings.dart index 9d1d03434f..2ba7f38716 100644 --- a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_bindings.dart +++ b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_bindings.dart @@ -89,6 +89,18 @@ class Node implements ffi.Finalizable { _activeFinalizerFn = null; } + /// Detaches the finalizer and invalidates this object, returning the + /// underlying C++ pointer. + /// + /// Throws a [StateError] if the object has already been disposed, or if + /// this object does not own the pointer. + ffi.Pointer detachPointer() { + final rawPtr = _ptr; + releaseOwnership(); + _ptr = ffi.nullptr; + return rawPtr; + } + factory Node(int value, ffi.Pointer destructorCounter) { return Node.fromPointer( _Node_new(value, destructorCounter), @@ -137,6 +149,149 @@ external int _Node_getValue(ffi.Pointer self); @ffi.Native)>(symbol: 'Node_delete') external void _Node_delete(ffi.Pointer self); +class NodeContainer implements ffi.Finalizable { + ffi.Pointer _ptr; + static final _defaultFinalizer = ffi.NativeFinalizer( + ffi.Native.addressOf< + ffi.NativeFunction)> + >(_NodeContainer_delete), + ); + + /// The finalizer currently attached for this instance, or [null] if this + /// object does not own its pointer. + ffi.NativeFinalizer? _activeFinalizer; + + /// The native function pointer used by [_activeFinalizer], stored so that + /// [dispose] can call the correct destructor directly. + ffi.Pointer)>>? + _activeFinalizerFn; + + NodeContainer.fromPointer(this._ptr, {bool takeOwnership = false}) { + if (takeOwnership) { + _defaultFinalizer.attach(this, _ptr.cast(), detach: this); + _activeFinalizer = _defaultFinalizer; + _activeFinalizerFn = + ffi.Native.addressOf< + ffi.NativeFunction)> + >(_NodeContainer_delete); + } + } + + /// Attaches a finalizer so this object takes ownership of the underlying + /// C++ pointer. If [customFinalizer] is provided it is used instead of the + /// default `delete` finalizer, which is useful when the object was not + /// allocated with `new` (e.g. `malloc` or a custom allocator). + /// + /// Both [customFinalizer] and [customFinalizerFn] must be provided together. + /// + /// Throws a [StateError] if the object has already been disposed, or if + /// this object already owns the pointer. + void retainOwnership([ + ffi.NativeFinalizer? customFinalizer, + ffi.Pointer)>>? + customFinalizerFn, + ]) { + if (_ptr == ffi.nullptr) { + throw StateError('This object has already been disposed.'); + } + if (_activeFinalizer != null) { + throw StateError('This object already owns its pointer.'); + } + if ((customFinalizer == null) != (customFinalizerFn == null)) { + throw ArgumentError( + 'Both customFinalizer and customFinalizerFn must be provided together.', + ); + } + final fin = customFinalizer ?? _defaultFinalizer; + final fnPtr = + customFinalizerFn ?? + ffi.Native.addressOf< + ffi.NativeFunction)> + >(_NodeContainer_delete); + fin.attach(this, _ptr.cast(), detach: this); + _activeFinalizer = fin; + _activeFinalizerFn = fnPtr; + } + + /// Detaches the finalizer so this object releases ownership of the + /// underlying C++ pointer. The caller becomes responsible for freeing + /// the memory. + /// + /// Throws a [StateError] if the object has already been disposed, or if + /// this object does not own the pointer. + void releaseOwnership() { + if (_ptr == ffi.nullptr) { + throw StateError('This object has already been disposed.'); + } + if (_activeFinalizer == null) { + throw StateError('This object does not own its pointer.'); + } + _activeFinalizer!.detach(this); + _activeFinalizer = null; + _activeFinalizerFn = null; + } + + /// Detaches the finalizer and invalidates this object, returning the + /// underlying C++ pointer. + /// + /// Throws a [StateError] if the object has already been disposed, or if + /// this object does not own the pointer. + ffi.Pointer detachPointer() { + final rawPtr = _ptr; + releaseOwnership(); + _ptr = ffi.nullptr; + return rawPtr; + } + + factory NodeContainer(Node node) { + return NodeContainer.fromPointer( + _NodeContainer_new(node.detachPointer()), + takeOwnership: true, + ); + } + int getValue() { + if (_ptr == ffi.nullptr) { + throw StateError('This object has already been disposed.'); + } + + return _NodeContainer_getValue(_ptr); + } + + void dispose() { + if (_ptr == ffi.nullptr) { + throw StateError('This object has already been disposed.'); + } + if (_activeFinalizer == null) { + throw StateError( + 'Cannot dispose a non-owning wrapper. ' + 'Call retainOwnership() first to take ownership.', + ); + } + _activeFinalizer!.detach(this); + _activeFinalizer = null; + _activeFinalizerFn?.asFunction)>()( + _ptr, + ); + _activeFinalizerFn = null; + _ptr = ffi.nullptr; + } +} + +@ffi.Native Function(ffi.Pointer)>( + symbol: 'NodeContainer_new', +) +external ffi.Pointer _NodeContainer_new(ffi.Pointer node); + +@ffi.Native)>( + symbol: 'NodeContainer_getValue', +) +external int _NodeContainer_getValue(ffi.Pointer self); + +@ffi.Native)>( + symbol: 'NodeContainer_delete', +) +external void _NodeContainer_delete(ffi.Pointer self); + class NodeManager implements ffi.Finalizable { ffi.Pointer _ptr; static final _defaultFinalizer = ffi.NativeFinalizer( @@ -219,6 +374,18 @@ class NodeManager implements ffi.Finalizable { _activeFinalizerFn = null; } + /// Detaches the finalizer and invalidates this object, returning the + /// underlying C++ pointer. + /// + /// Throws a [StateError] if the object has already been disposed, or if + /// this object does not own the pointer. + ffi.Pointer detachPointer() { + final rawPtr = _ptr; + releaseOwnership(); + _ptr = ffi.nullptr; + return rawPtr; + } + factory NodeManager() { return NodeManager.fromPointer(_NodeManager_new(), takeOwnership: true); } @@ -276,6 +443,25 @@ class NodeManager implements ffi.Finalizable { return _NodeManager_takeNode(_ptr, node._ptr); } + Node makeNode(int value, ffi.Pointer destructorCounter) { + if (_ptr == ffi.nullptr) { + throw StateError('This object has already been disposed.'); + } + + return Node.fromPointer( + _NodeManager_makeNode(_ptr, value, destructorCounter), + takeOwnership: true, + ); + } + + int consumeNode(Node node) { + if (_ptr == ffi.nullptr) { + throw StateError('This object has already been disposed.'); + } + + return _NodeManager_consumeNode(_ptr, node.detachPointer()); + } + void dispose() { if (_ptr == ffi.nullptr) { throw StateError('This object has already been disposed.'); @@ -362,6 +548,27 @@ external int _NodeManager_takeNode( ffi.Pointer node, ); +@ffi.Native< + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ) +>(symbol: 'NodeManager_makeNode') +external ffi.Pointer _NodeManager_makeNode( + ffi.Pointer self, + int value, + ffi.Pointer destructorCounter, +); + +@ffi.Native, ffi.Pointer)>( + symbol: 'NodeManager_consumeNode', +) +external int _NodeManager_consumeNode( + ffi.Pointer self, + ffi.Pointer node, +); + @ffi.Native)>( symbol: 'NodeManager_delete', ) diff --git a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_bindings.dart.cpp b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_bindings.dart.cpp index ab818c1825..d85178a239 100644 --- a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_bindings.dart.cpp +++ b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_bindings.dart.cpp @@ -1,3 +1,4 @@ +#include #include "memory_edge_cases.h" #if defined(_WIN32) @@ -20,6 +21,18 @@ FFIGEN_EXPORT void Node_delete(Node* self) { delete self; } +FFIGEN_EXPORT NodeContainer* NodeContainer_new(Node* node) { + return new NodeContainer(std::unique_ptr(node)); +} + +FFIGEN_EXPORT int NodeContainer_getValue(const NodeContainer* self) { + return self->getValue(); +} + +FFIGEN_EXPORT void NodeContainer_delete(NodeContainer* self) { + delete self; +} + FFIGEN_EXPORT NodeManager* NodeManager_new() { return new NodeManager(); } @@ -48,6 +61,14 @@ FFIGEN_EXPORT int NodeManager_takeNode(NodeManager* self, Node* node) { return self->takeNode(node); } +FFIGEN_EXPORT Node* NodeManager_makeNode(NodeManager* self, int value, int * destructorCounter) { + return self->makeNode(value, destructorCounter).release(); +} + +FFIGEN_EXPORT int NodeManager_consumeNode(NodeManager* self, Node* node) { + return self->consumeNode(std::unique_ptr(node)); +} + FFIGEN_EXPORT void NodeManager_delete(NodeManager* self) { delete self; } diff --git a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_test.dart b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_test.dart index d1c6501ca8..7c32bb9009 100644 --- a/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_test.dart +++ b/pkgs/ffigen/test/native_cpp_test/memory_edge_cases_test.dart @@ -150,4 +150,162 @@ void main() { calloc.free(counter); }); }); + + group('unique_ptr memory management', () { + test('makeNode() returns owned Node (takeOwnership: true)', () { + final counter = calloc()..value = 0; + final manager = NodeManager(); + final node = manager.makeNode(42, counter.cast()); + + expect(node.getValue(), 42); + expect(counter.value, 0); // not destroyed yet + + node.dispose(); + expect(counter.value, 1); // destructor called on dispose + calloc.free(counter); + }); + + test('makeNode() Node is GC-destroyed automatically', () { + final counter = calloc()..value = 0; + + @pragma('vm:never-inline') + void inner() { + final manager = NodeManager(); + // ignore: unused_local_variable + final _ = manager.makeNode(99, counter.cast()); + } + + inner(); + doGC(); + expect(counter.value, 1); + calloc.free(counter); + }, skip: !canDoGC); + + test('makeNode() cannot call retainOwnership() again (already owned)', () { + final counter = calloc()..value = 0; + final manager = NodeManager(); + final node = manager.makeNode(1, counter.cast()); + expect(node.retainOwnership, throwsStateError); + node.dispose(); + calloc.free(counter); + }); + + test( + 'consumeNode() transfers ownership, destroys object, returns value', + () { + final counter = calloc()..value = 0; + final manager = NodeManager(); + final node = manager.makeNode(77, counter.cast()); + expect(counter.value, 0); // not yet destroyed + + final val = manager.consumeNode(node); + expect(val, 77); + expect(counter.value, 1); // unique_ptr destructor fired inside C++ + calloc.free(counter); + }, + ); + + test('consumeNode() invalidates the Dart wrapper', () { + final counter = calloc()..value = 0; + final manager = NodeManager(); + final node = manager.makeNode(55, counter.cast()); + + manager.consumeNode(node); + + // Wrapper must be in disposed state after transfer. + expect(node.getValue, throwsStateError); + calloc.free(counter); + }); + + test('double consumeNode() throws StateError on second call', () { + final counter = calloc()..value = 0; + final manager = NodeManager(); + final node = manager.makeNode(33, counter.cast()); + + manager.consumeNode(node); // first call: succeeds + expect( + () => manager.consumeNode(node), + throwsStateError, + ); // second call: wrapper is nullptr + calloc.free(counter); + }); + + test('consumeNode() with non-owning wrapper throws StateError', () { + final counter = calloc()..value = 0; + final rawPtr = _rawNodeNew(111, counter.cast()); + final node = Node.fromPointer(rawPtr, takeOwnership: false); + final manager = NodeManager(); + + expect(() => manager.consumeNode(node), throwsStateError); + + // Wrapper must be unmodified after the failed call. + expect(node.getValue(), 111); + expect(counter.value, 0); + + _rawNodeDelete(rawPtr); + calloc.free(counter); + }); + + test('consumeNode() with already-disposed wrapper throws StateError', () { + final counter = calloc()..value = 0; + final manager = NodeManager(); + final node = manager.makeNode(222, counter.cast()); + + node.dispose(); + expect(counter.value, 1); // already destroyed by dispose() + + expect(() => manager.consumeNode(node), throwsStateError); + calloc.free(counter); + }); + + test('retainOwnership() then consumeNode() works correctly', () { + final counter = calloc()..value = 0; + final rawPtr = _rawNodeNew(333, counter.cast()); + final node = Node.fromPointer(rawPtr, takeOwnership: false) + ..retainOwnership(); + + final manager = NodeManager(); + final val = manager.consumeNode(node); // should succeed + + expect(val, 333); + expect(counter.value, 1); // C++ destroyed it inside consumeNode + // Dart wrapper is now invalid. + expect(node.getValue, throwsStateError); + calloc.free(counter); + }); + + test( + 'Constructor taking std::unique_ptr transfers ownership correctly', + () { + final counter = calloc()..value = 0; + final manager = NodeManager(); + final node = manager.makeNode(444, counter.cast()); + + final container = NodeContainer(node); + expect(container.getValue(), 444); + // node was transferred, so node wrapper is now disposed + expect(node.getValue, throwsStateError); + + container.dispose(); + expect( + counter.value, + 1, + ); // C++ destroyed Node when container was destroyed + calloc.free(counter); + }, + ); + + test('Constructor taking std::unique_ptr with non-owning wrapper ' + 'throws StateError', () { + final counter = calloc()..value = 0; + final rawPtr = _rawNodeNew(555, counter.cast()); + final node = Node.fromPointer(rawPtr, takeOwnership: false); + + expect(() => NodeContainer(node), throwsStateError); + expect(node.getValue(), 555); + + _rawNodeDelete(rawPtr); + calloc.free(counter); + }); + }); } diff --git a/pkgs/ffigen/test/native_cpp_test/verify_bindings_test.dart b/pkgs/ffigen/test/native_cpp_test/verify_bindings_test.dart index 040580a872..d167338ed8 100644 --- a/pkgs/ffigen/test/native_cpp_test/verify_bindings_test.dart +++ b/pkgs/ffigen/test/native_cpp_test/verify_bindings_test.dart @@ -61,7 +61,13 @@ void main() { ], compilerOptions: ['-x', 'c++'], ), - cpp: Cpp(classes: CppClasses.includeSet({'Node', 'NodeManager'})), + cpp: Cpp( + classes: CppClasses.includeSet({ + 'Node', + 'NodeManager', + 'NodeContainer', + }), + ), ), }; diff --git a/pkgs/ffigen/tool/libclang_config.yaml b/pkgs/ffigen/tool/libclang_config.yaml index bc8fad04b7..5dd1509bc8 100644 --- a/pkgs/ffigen/tool/libclang_config.yaml +++ b/pkgs/ffigen/tool/libclang_config.yaml @@ -140,6 +140,8 @@ functions: - clang_getClangVersion - clang_Type_getNumObjCProtocolRefs - clang_Type_getObjCProtocolDecl + - clang_Type_getNumTemplateArguments + - clang_Type_getTemplateArgumentAsType # time_t typedef varies between platforms, and we don't need it anyway. typedefs: