Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
60 changes: 51 additions & 9 deletions pkgs/ffigen/lib/src/code_generator/cpp_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CppMethod extends AstNode with HasLocalScope {
final bool isConstant;
final bool isStatic;
final CppMethodKind kind;
final CppClass? originatingClass;

CppMethod({
required this.name,
Expand All @@ -33,10 +34,33 @@ class CppMethod extends AstNode with HasLocalScope {
required this.isConstant,
this.isStatic = false,
this.kind = CppMethodKind.method,
this.originatingClass,
});

bool get isConstructor => kind == .constructor;

CppMethod cloneForClass(CppClass targetClass, CppClass baseClass) {
return CppMethod(
name: Symbol(
'${targetClass.originalName}_$originalName',

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.

I was pretty confused about this name mangling until I realised that you're using originalName instead of name when generating the Dart method. That means that users won't be able to rename methods, and the name collision resolution logic won't work.

You don't need to fix that in this PR, but I filed a bug so we don't forget: #3552

SymbolKind.method,
),
originalName: originalName,
returnType: returnType,
parameters: parameters.map((p) => p.clone()).toList(),
isConstant: isConstant,
isStatic: isStatic,
kind: kind,
originatingClass: baseClass,
);
}

String signatureKey() {
final paramTypes = parameters.map((p) => p.type.cacheKey()).join(',');
final constSuffix = isConstant ? ' const' : '';
return '$originalName($paramTypes)$constSuffix';
}

@override
void visit(Visitation visitation) => visitation.visitCppMethod(this);

Expand All @@ -46,6 +70,7 @@ class CppMethod extends AstNode with HasLocalScope {
visitor.visit(name);
visitor.visit(returnType);
visitor.visitAll(parameters);
visitor.visit(originatingClass);
}
}

Expand All @@ -71,6 +96,12 @@ class CppClass extends BindingType with HasLocalScope {
final List<CppMethod> methods;
final List<CppMember> fields;

/// The public C++ base classes for this class, in declaration order.
///
/// Only public inheritance is represented here. Protected and private bases
/// are silently ignored by the parser.
final List<CppClass> bases;

CppClass({
super.usr,
super.originalName,
Expand All @@ -79,6 +110,7 @@ class CppClass extends BindingType with HasLocalScope {
required this.context,
required this.methods,
required this.fields,
this.bases = const [],
});

@override
Expand All @@ -96,6 +128,11 @@ class CppClass extends BindingType with HasLocalScope {
required LocalVariables localVariables,
}) => '$value._ptr';

void copyMethod(CppMethod method, CppClass originatingBase) {
final cloned = method.cloneForClass(this, originatingBase);
methods.add(cloned);
}

@override
BindingString toBindingString(Writer w) {
final s = StringBuffer();
Expand All @@ -115,8 +152,13 @@ class CppClass extends BindingType with HasLocalScope {
final deleteGlue = '_$deleteSymbol';

s.write(makeDartDoc(dartDoc));
// Build the implements clause: ffi.Finalizable + public base classes.
final implementsClause = [
'$ffiPrefix.Finalizable',
...bases.map((b) => b.name),
].join(', ');
s.write('''
class $name implements $ffiPrefix.Finalizable {
class $name implements $implementsClause {
$ptrVoid _ptr;
''');

Expand Down Expand Up @@ -287,7 +329,7 @@ class $name implements $ffiPrefix.Finalizable {
}
}
s.write('''
void dispose() {
${bases.isNotEmpty ? '@override\n ' : ''}void dispose() {
if (_ptr == $ffiPrefix.nullptr) {
throw StateError('This object has already been disposed.');
}
Expand All @@ -302,6 +344,7 @@ class $name implements $ffiPrefix.Finalizable {
_ptr = $ffiPrefix.nullptr;
}
''');

s.write('}\n');

// Writes a @Native annotation + external declaration for a glue function.
Expand Down Expand Up @@ -403,17 +446,15 @@ FFIGEN_EXPORT void ${name}_delete($originalName* self) {
final otherParams = method.parameters.map(paramDecl);

if (method.isStatic) {
final targetType =
method.originatingClass?.originalName ?? originalName;
params = otherParams.join(', ');
body =
'$returnPrefix$originalName::'
'$returnPrefix$targetType::'
'${method.originalName}($callArgs);';
} else {
final String selfType;
if (method.isConstant) {
selfType = 'const $originalName';
} else {
selfType = originalName;
}
final constPrefix = method.isConstant ? 'const ' : '';
final selfType = '$constPrefix$originalName';
params = ['$selfType* self', ...otherParams].join(', ');
final methodName = method.originalName;
final suffix = method.returnType is CppUniquePtrType
Expand Down Expand Up @@ -456,6 +497,7 @@ FFIGEN_EXPORT $returnTypeString $symbol($params) {
super.visitChildren(visitor);
visitor.visitAll(methods);
visitor.visitAll(fields);
visitor.visitAll(bases);
visitor.visit(ffiImport);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,22 @@ class Clang {
late final _clang_getCString = _clang_getCStringPtr
.asFunction<ffi.Pointer<ffi.Char> Function(CXString)>();

/// Returns the access control level for the referenced object.
///
/// If the cursor refers to a C++ declaration, its access control level within its
/// parent scope is returned. Otherwise, if the cursor refers to a base specifier or
/// access specifier, the specifier itself is returned.
int clang_getCXXAccessSpecifier(CXCursor arg0) {
return _clang_getCXXAccessSpecifier(arg0);
}

late final _clang_getCXXAccessSpecifierPtr =
_lookup<ffi.NativeFunction<ffi.UnsignedInt Function(CXCursor)>>(
'clang_getCXXAccessSpecifier',
);
late final _clang_getCXXAccessSpecifier = _clang_getCXXAccessSpecifierPtr
.asFunction<int Function(CXCursor)>();

/// Return the canonical type for a CXType.
///
/// Clang's type system explicitly models typedefs and all the ways
Expand Down Expand Up @@ -3042,6 +3058,15 @@ sealed class CXVisitorResult {
static const CXVisit_Continue = 1;
}

/// Represents the C++ access control level to a base class for a
/// cursor with kind CX_CXXBaseSpecifier.
sealed class CX_CXXAccessSpecifier {
static const CX_CXXInvalidAccessSpecifier = 0;
static const CX_CXXPublic = 1;
static const CX_CXXProtected = 2;
static const CX_CXXPrivate = 3;
}

/// Represents the storage classes as declared in the source. CX_SC_Invalid
/// was added for the case that the passed cursor in not a declaration.
sealed class CX_StorageClass {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ CppClass? parseClassDeclaration(Context context, clang_types.CXCursor cursor) {
}
});

// Parse public base classes (only public specifiers; non-public are ignored).
final bases = _parsePublicBases(context, cursor);

final cppClass = CppClass(
usr: usr,
dartDoc: getCursorDocComment(
Expand All @@ -76,13 +79,41 @@ CppClass? parseClassDeclaration(Context context, clang_types.CXCursor cursor) {
context: context,
methods: methods,
fields: <CppMember>[],
bases: bases,
);

context.bindingsIndex.addCppClassToSeen(usr, cppClass);

return cppClass;
}

/// Parses the direct public base classes of [cursor].
List<CppClass> _parsePublicBases(Context context, clang_types.CXCursor cursor) {
final bases = <CppClass>[];

cursor.visitChildren((child) {
final kind = clang.clang_getCursorKind(child);
if (kind != clang_types.CXCursorKind.CXCursor_CXXBaseSpecifier) return;

final access = clang.clang_getCXXAccessSpecifier(child);
if (access != clang_types.CX_CXXAccessSpecifier.CX_CXXPublic) return;

final baseType = clang.clang_getCursorType(child);
final baseDeclCursor = clang.clang_getTypeDeclaration(baseType);
final baseUsr = baseDeclCursor.usr();

final baseClass = context.bindingsIndex.getSeenCppClass(baseUsr);
if (baseClass == null) {
final parsed = parseClassDeclaration(context, baseDeclCursor);
if (parsed != null) bases.add(parsed);
} else {
bases.add(baseClass);
}
});

return bases;
}

void _parseAnyMethod(
Context context,
clang_types.CXCursor cursor,
Expand Down
29 changes: 29 additions & 0 deletions pkgs/ffigen/lib/src/visitor/copy_methods_from_super_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,35 @@ const _excludedNSObjectMethods = {
};

class CopyMethodsFromSuperTypesVisitation extends Visitation {
@override
void visitCppClass(CppClass node) {
node.visitChildren(visitor);

final existingSignatures = node.methods
.map((m) => m.signatureKey())
.toSet();

_copyCppMethodsFromBase(node, node, existingSignatures);
}

void _copyCppMethodsFromBase(
CppClass target,
CppClass current,
Set<String> existingSignatures,
) {
for (final method in current.methods) {
if (method.kind == CppMethodKind.constructor) continue;

if (existingSignatures.add(method.signatureKey())) {
target.copyMethod(method, current);
}
}

for (final base in current.bases) {
_copyCppMethodsFromBase(target, base, existingSignatures);
}
}

@override
void visitObjCInterface(ObjCInterface node) {
node.visitChildren(visitor, typeGraphOnly: true);
Expand Down
60 changes: 60 additions & 0 deletions pkgs/ffigen/test/native_cpp_test/cpp_inheritance_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// 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 "cpp_inheritance_test.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

Shape::Shape(double x, double y) : x_(x), y_(y) {}
Shape::~Shape() {}
double Shape::getX() const { return x_; }
double Shape::getY() const { return y_; }

Drawable::Drawable() : drawCount_(0) {}
Drawable::~Drawable() {}
int Drawable::draw() const { return 42; }

Circle::Circle(double x, double y, double radius)
: Shape(x, y), radius_(radius) {}

double Circle::area() const {
return M_PI * radius_ * radius_;
}

ColoredCircle::ColoredCircle(double x, double y, double radius, int color)
: Circle(x, y, radius), Drawable(), color_(color) {}

int ColoredCircle::getColor() const { return color_; }

Square::Square(double x, double y, double side)
: Shape(x, y), side_(side) {}

double Square::getX() const { return Shape::getX() + side_; }

double Square::area() const { return side_ * side_; }

int AccessBase::value() const { return v_; }
PublicDerived::PublicDerived() {}
ProtectedDerived::ProtectedDerived() {}
PrivateDerived::PrivateDerived() {}

OverloadBase::OverloadBase() {}
OverloadBase::~OverloadBase() {}
int OverloadBase::getValue(int x) { return x * 2; }
double OverloadBase::getValueDouble(double x) { return x * 3.0; }

OverloadDerived::OverloadDerived() {}
int OverloadDerived::getValue(int x) { return x * 10; }

DiamondBase::DiamondBase() {}
DiamondBase::~DiamondBase() {}
int DiamondBase::baseVal() const { return 42; }
int DiamondBase::virtVal() const { return 100; }

DiamondLeft::DiamondLeft() {}
int DiamondLeft::virtVal() const { return 200; }
DiamondRight::DiamondRight() {}
DiamondDerived::DiamondDerived() {}
Loading
Loading