Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 87 additions & 12 deletions pkgs/ffigen/lib/src/code_generator/cpp_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -202,22 +213,42 @@ class $name implements $ffiPrefix.Finalizable {

final dartParams = dartParamList(ctor.parameters);

final ownedParams = ctor.parameters
.where((p) => p.type is CppUniquePtrType)
.toList();

final localVars = LocalVariables(ctor.localScope);

final rawPtrVars = <String, String>{};
for (final p in ownedParams) {
rawPtrVars[p.name] = '_raw_${p.name}';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating these sorts of variables is exactly what LocalVariables is for. It would be better to use that. But actually, now that you've got the detachPointer method, you can probably inline it, and get rid of these local variables.

}

final callArgs = ctor.parameters
.map(
(p) => p.type.convertDartTypeToFfiDartType(
.map((p) {
if (rawPtrVars.containsKey(p.name)) {
return rawPtrVars[p.name]!;
}
return p.type.convertDartTypeToFfiDartType(
ctx,
p.name,
objCRetain: false,
objCAutorelease: false,
localVariables: localVars,
),
)
);
})
.join(', ');

final ownershipChecks = StringBuffer();
for (final p in ownedParams) {
final raw = rawPtrVars[p.name]!;
ownershipChecks.write(' final $raw = ${p.name}.detachPointer();\n');
}

s.write('''
factory $name($dartParams) {
${localVars.generateDeclarations()}
${ownershipChecks.toString().trimLeft()}
return $name.fromPointer($privateName($callArgs), takeOwnership: true);
}
''');
Expand All @@ -228,18 +259,31 @@ class $name implements $ffiPrefix.Finalizable {
final dartReturn = method.returnType.getDartType(ctx);
final dartParams = dartParamList(method.parameters);

final ownedParams = method.parameters
.where((p) => p.type is CppUniquePtrType)
.toList();

final localVars = LocalVariables(method.localScope);

final rawPtrVars = <String, String>{};
for (final p in ownedParams) {
rawPtrVars[p.name] = '_raw_${p.name}';
}

final callArgs = [
if (!method.isStatic) '_ptr',
...method.parameters.map(
(p) => p.type.convertDartTypeToFfiDartType(
...method.parameters.map((p) {
if (rawPtrVars.containsKey(p.name)) {
return rawPtrVars[p.name]!;
}
return p.type.convertDartTypeToFfiDartType(
ctx,
p.name,
objCRetain: false,
objCAutorelease: false,
localVariables: localVars,
),
),
);
}),
].join(', ');
final decls = localVars.generateDeclarations();

Expand All @@ -249,21 +293,38 @@ class $name implements $ffiPrefix.Finalizable {
objCRetain: false,
);

// Build the ownership-transfer preamble for owned parameters.
final ownershipChecks = StringBuffer();
for (final p in ownedParams) {
final raw = rawPtrVars[p.name]!;
ownershipChecks.write(' final $raw = ${p.name}.detachPointer();\n');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detachPointer() call should happen in CppUniquePtrType.convertDartTypeToFfiDartType

}

final hasReturn = method.returnType != voidType;

if (method.isStatic) {
final callLine = hasReturn
? 'return $returnExpr;'
: '$glue($callArgs);';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can $glue($callArgs) be replaced with returnExpr?

s.write('''\
static $dartReturn ${method.originalName}($dartParams) {
$decls
return $returnExpr;
${ownershipChecks.toString().trimLeft()}
$callLine
}
''');
} else {
final callLine = hasReturn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This callLine variable is identical between both branches of this if statement, so you could deduplicate it by just moving it up next to the definition of hasReturn.

? 'return $returnExpr;'
: '$glue($callArgs);';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can $glue($callArgs) be replaced with returnExpr?

s.write('''\
$dartReturn ${method.originalName}($dartParams) {
if (_ptr == $ffiPrefix.nullptr) {
throw StateError('This object has already been disposed.');
}
$decls
return $returnExpr;
${ownershipChecks.toString().trimLeft()}
$callLine
}
''');
}
Expand Down Expand Up @@ -365,12 +426,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(', ');
Expand All @@ -396,7 +458,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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now, but you're probably going to run into more places where you need to reuse this return type conversion (and the _cppCallArg util), eg when adding support for top-level functions with C++ signatures.

I filed a bug to clean this up later, don't worry about it at the moment: #3523

? '.release()'
: '';
body = '${returnPrefix}self->$methodName($callArgs)$suffix;';
}
}

Expand Down Expand Up @@ -436,3 +502,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;
}
21 changes: 19 additions & 2 deletions pkgs/ffigen/lib/src/code_generator/pointer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,6 @@ class ObjCObjectPointerWithProtocols extends ObjCObjectPointer {
}

/// A pointer to a C++ class wrapper object.
/// Returned pointers are always unowned by default. The developer must call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restore this deleted comment?

/// `retainOwnership()` explicitly if ownership has been transferred.
class CppClassPointerType extends PointerType {
final CppClass cppClass;

Expand Down Expand Up @@ -314,3 +312,22 @@ class CppClassPointerType extends PointerType {
visitor.visit(ffiImport);
}
}

/// A type representing `std::unique_ptr<T>` ownership transfer.
class CppUniquePtrType extends CppClassPointerType {
CppUniquePtrType(super.cppClass);

@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()}>';
}
1 change: 1 addition & 0 deletions pkgs/ffigen/lib/src/code_generator/writer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ id objc_retainBlock(id);
String? generateCpp(String outFilename) {
final s = StringBuffer();
final outDir = p.dirname(outFilename);
s.write('#include <memory>\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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,19 @@ class Clang {
late final _clang_Type_getNumObjCProtocolRefs =
_clang_Type_getNumObjCProtocolRefsPtr.asFunction<int Function(CXType)>();

/// 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<ffi.NativeFunction<ffi.Int Function(CXType)>>(
'clang_Type_getNumTemplateArguments',
);
late final _clang_Type_getNumTemplateArguments =
_clang_Type_getNumTemplateArgumentsPtr.asFunction<int Function(CXType)>();

/// Retrieves the base type of the ObjCObjectType.
///
/// If the type is not an ObjC object, an invalid type is returned.
Expand Down Expand Up @@ -501,6 +514,23 @@ class Clang {
_clang_Type_getObjCProtocolDeclPtr
.asFunction<CXCursor Function(CXType, int)>();

/// 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<ffi.NativeFunction<CXType Function(CXType, ffi.UnsignedInt)>>(
'clang_Type_getTemplateArgumentAsType',
);
late final _clang_Type_getTemplateArgumentAsType =
_clang_Type_getTemplateArgumentAsTypePtr
.asFunction<CXType Function(CXType, int)>();

/// Provides a shared context for creating translation units.
///
/// It provides two options:
Expand Down
56 changes: 56 additions & 0 deletions pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ 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 = clang.clang_getCursorUSR(declCursor).toStringAndDispose();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have extension methods for this. You can use declCursor.usr().

final isStdUniquePtr = usr.contains('std@') && usr.contains('unique_ptr');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show me what one of these USRs looks like? Doing .contains here is a little odd. I would have thought you could just do final isStdUniquePtr = usr == "some string literal";. Is there a reason that won't work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libclang produces USRs in the following format:

c:@N@std@S@unique_ptr>#$@S@Node#$@N@std@S@default_delete>#S0_
So i think libclang appends the template argument types (such as >#$@s@Node...) directly into the USR string for each template instantiation, the full USR varies depending on the type parameter T.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Can you do usr.startsWith("c:@N@std@S@unique_ptr>#")?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

if (isStdUniquePtr) {
final spelling = clang

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cxtype.spelling()

.clang_getTypeSpelling(cxtype)
.toStringAndDispose();
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) {
Expand Down Expand Up @@ -287,6 +308,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,
Expand Down
30 changes: 27 additions & 3 deletions pkgs/ffigen/test/native_cpp_test/cpp_class_test_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<ffi.Void> detachPointer() {
final rawPtr = _ptr;
releaseOwnership();
_ptr = ffi.nullptr;
return rawPtr;
}

factory Animal(int age) {
return Animal.fromPointer(_Animal_new(age), takeOwnership: true);
}
Expand All @@ -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() {
Expand All @@ -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() {
Expand Down Expand Up @@ -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<ffi.Void> detachPointer() {
final rawPtr = _ptr;
releaseOwnership();
_ptr = ffi.nullptr;
return rawPtr;
}

factory FinalizerTestSubject(ffi.Pointer<ffi.Int> counter) {
return FinalizerTestSubject.fromPointer(
_FinalizerTestSubject_new(counter),
Expand Down
Loading
Loading