From e47f807a4c147825d47525fd1b4fc9695b96b318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Korde=C5=BE?= Date: Wed, 1 Jul 2026 15:18:37 +0200 Subject: [PATCH 1/5] Add support for C++ enums in namespaces with flattened Dart names --- .../sub_parsers/enumdecl_parser.dart | 41 ++++++++++++++-- .../translation_unit_parser.dart | 48 ++++++++++++++++++ .../cpp_namespace_enum_test.dart | 33 +++++++++++++ .../native_cpp_test/cpp_namespace_enum_test.h | 19 +++++++ .../cpp_namespace_enum_test_bindings.dart | 49 +++++++++++++++++++ .../native_cpp_test/verify_bindings_test.dart | 16 ++++++ 6 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.dart create mode 100644 pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.h create mode 100644 pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test_bindings.dart diff --git a/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart index ffd58fa9c5..35fec5324b 100644 --- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart +++ b/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart @@ -55,7 +55,19 @@ EnumClass parseEnumDeclaration(clang_types.CXCursor cursor, Context context) { .where((c) => c.rawValue.startsWith('-')) .isNotEmpty; } else { - final decl = Declaration(usr: usr, originalName: enumName); + // For C++ enums declared inside one or more namespaces, [qualifiedName] is + // the fully-qualified name (e.g. `outer::inner::Color`). At global scope it + // equals [enumName]. + final qualifiedName = _qualifiedName(usr, enumName); + final decl = Declaration(usr: usr, originalName: qualifiedName); + var dartName = config.enums.rename(decl); + if (dartName.contains('::')) { + // The default `rename` returns the (qualified) original name unchanged, + // which isn't a valid Dart identifier. Flatten the namespace path into a + // single name joined by `$`, e.g. `outer::inner::Color` becomes + // `outer$inner$Color`. + dartName = _flattenNamespace(qualifiedName); + } logger.fine('++++ Adding Enum: ${cursor.completeStringRepr()}'); enumClass = EnumClass( usr: usr, @@ -64,8 +76,8 @@ EnumClass parseEnumDeclaration(clang_types.CXCursor cursor, Context context) { cursor, availability: apiAvailability.dartDoc, ), - originalName: enumName, - name: config.enums.rename(decl), + originalName: qualifiedName, + name: dartName, nativeType: nativeType, context: context, apiAvailability: apiAvailability, @@ -132,3 +144,26 @@ EnumClass parseEnumDeclaration(clang_types.CXCursor cursor, Context context) { apiAvailability: apiAvailability, ); } + +/// Builds the fully-qualified C++ name of an enum from its [usr]. +/// +/// A USR like `c:@N@outer@N@inner@E@Color` yields `outer::inner::Color`. At +/// global scope (no enclosing namespace) this just returns [leafName]. +String _qualifiedName(String usr, String leafName) { + // After the `c:` prefix, USR tokens alternate between a single-char kind + // marker (`N` for namespace, `E` for enum, etc.) and its name. Collect the + // names of the enclosing namespaces by stepping over each marker/name pair. + final parts = usr.split('@'); + final namespaces = []; + for (var i = 1; i + 1 < parts.length; i += 2) { + if (parts[i] == 'N') namespaces.add(parts[i + 1]); + } + if (namespaces.isEmpty) return leafName; + return [...namespaces, leafName].join('::'); +} + +/// Flattens a `::`-qualified C++ name into a single Dart identifier by joining +/// the path segments with `$`, e.g. `outer::inner::Color` becomes +/// `outer$inner$Color`. +String _flattenNamespace(String qualifiedName) => + qualifiedName.split('::').where((s) => s.isNotEmpty).join(r'$'); diff --git a/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart b/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart index 8d4022f31d..36f773694a 100644 --- a/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart +++ b/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart @@ -61,6 +61,9 @@ Set parseTranslationUnit( case clang_types.CXCursorKind.CXCursor_ClassDecl: addToBindings(bindings, parseClassDeclaration(context, cursor)); break; + case clang_types.CXCursorKind.CXCursor_Namespace: + _visitNamespaceForEnums(context, cursor, bindings, headers); + break; default: logger.finer('rootCursorVisitor: CursorKind not implemented'); } @@ -87,6 +90,51 @@ void addToBindings(Set bindings, Binding? b) { } } +/// Recurses into a C++ namespace, surfacing only enum declarations. +/// +/// For now this is the only declaration kind generated from inside namespaces. +// TODO: Dispatch ClassDecl, FunctionDecl, etc. here for full C++ namespace +// support. +void _visitNamespaceForEnums( + Context context, + clang_types.CXCursor namespaceCursor, + Set bindings, + Map headers, +) { + final logger = context.logger; + if (clang.clang_Cursor_isAnonymous(namespaceCursor) != 0) { + logger.fine('Skipping anonymous namespace.'); + return; + } + namespaceCursor.visitChildren((cursor) { + final file = cursor.sourceFileName(); + if (file.isEmpty) return; + if (!(headers[file] ??= context.config.headers.include(Uri.file(file)))) { + logger.finest( + 'namespaceCursorVisitor:(not included) ${cursor.completeStringRepr()}', + ); + return; + } + try { + logger.finest('namespaceCursorVisitor: ${cursor.completeStringRepr()}'); + switch (clang.clang_getCursorKind(cursor)) { + case clang_types.CXCursorKind.CXCursor_Namespace: + _visitNamespaceForEnums(context, cursor, bindings, headers); + break; + case clang_types.CXCursorKind.CXCursor_EnumDecl: + addToBindings(bindings, _getCodeGenTypeFromCursor(context, cursor)); + break; + default: + logger.finer('namespaceCursorVisitor: CursorKind not implemented'); + } + } catch (e, s) { + logger.severe(e); + logger.severe(s); + rethrow; + } + }); +} + BindingType? _getCodeGenTypeFromCursor( Context context, clang_types.CXCursor cursor, diff --git a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.dart b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.dart new file mode 100644 index 0000000000..1b55bcc328 --- /dev/null +++ b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.dart @@ -0,0 +1,33 @@ +// 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. + +import 'package:test/test.dart'; + +import 'cpp_namespace_enum_test_bindings.dart'; + +void main() { + group('CppNamespaceEnum', () { + test('enums in namespaces are generated with flattened names', () { + expect(outer$Color, isNotNull); + expect(outer$inner$Color, isNotNull); + expect(other$Color, isNotNull); + }); + + test('unscoped enum in a single namespace', () { + expect(outer$Color.red.value, 0); + expect(outer$Color.green.value, 1); + expect(outer$Color.blue.value, 2); + }); + + test('scoped enum (enum class) in a nested namespace', () { + expect(outer$inner$Color.cyan.value, 10); + expect(outer$inner$Color.magenta.value, 20); + }); + + test('leaf-name collision across namespaces is disambiguated', () { + expect(other$Color.black.value, 100); + expect(other$Color.white.value, 200); + }); + }); +} diff --git a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.h b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.h new file mode 100644 index 0000000000..fb4dabd46e --- /dev/null +++ b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.h @@ -0,0 +1,19 @@ +// 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. + +namespace outer { +// Unscoped enum in a single namespace. +enum Color { red, green, blue }; + +namespace inner { +// Scoped enum (`enum class`) in a nested namespace. Shares the leaf name +// `Color` with `outer::Color` and `other::Color`. +enum class Color { cyan = 10, magenta = 20 }; +} // namespace inner +} // namespace outer + +namespace other { +// Another `Color`, in a different namespace, to exercise leaf-name collisions. +enum Color { black = 100, white = 200 }; +} // namespace other diff --git a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test_bindings.dart b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test_bindings.dart new file mode 100644 index 0000000000..231a828daa --- /dev/null +++ b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test_bindings.dart @@ -0,0 +1,49 @@ +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint, unused_import, unused_element, deprecated_member_use_from_same_package +import 'dart:ffi' as ffi; + +enum other$Color { + black(100), + white(200); + + final int value; + const other$Color(this.value); + + static other$Color fromValue(int value) => switch (value) { + 100 => black, + 200 => white, + _ => throw ArgumentError('Unknown value for other\$Color: $value'), + }; +} + +enum outer$Color { + red(0), + green(1), + blue(2); + + final int value; + const outer$Color(this.value); + + static outer$Color fromValue(int value) => switch (value) { + 0 => red, + 1 => green, + 2 => blue, + _ => throw ArgumentError('Unknown value for outer\$Color: $value'), + }; +} + +enum outer$inner$Color { + cyan(10), + magenta(20); + + final int value; + const outer$inner$Color(this.value); + + static outer$inner$Color fromValue(int value) => switch (value) { + 10 => cyan, + 20 => magenta, + _ => throw ArgumentError('Unknown value for outer\$inner\$Color: $value'), + }; +} 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 09c0d68c3b..1de24b30ea 100644 --- a/pkgs/ffigen/test/native_cpp_test/verify_bindings_test.dart +++ b/pkgs/ffigen/test/native_cpp_test/verify_bindings_test.dart @@ -48,6 +48,22 @@ void main() { classes: CppClasses.includeSet({'Animal', 'FinalizerTestSubject'}), ), ), + 'cpp_namespace_enum': FfiGenerator( + output: Output( + dartFile: Uri.file('cpp_namespace_enum_test_bindings.dart'), + ), + headers: Headers( + entryPoints: [ + Uri.file(path.join(testDir.path, 'cpp_namespace_enum_test.h')), + ], + compilerOptions: ['-x', 'c++'], + ), + enums: Enums.includeSet({ + 'outer::Color', + 'outer::inner::Color', + 'other::Color', + }), + ), }; for (final testFile in testFiles) { From 69189673c1b79cac28959bdde57087b07d416dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Korde=C5=BE?= Date: Wed, 1 Jul 2026 18:50:59 +0200 Subject: [PATCH 2/5] Support nested enums in classes and structs --- .../sub_parsers/enumdecl_parser.dart | 34 +++++++------- .../translation_unit_parser.dart | 46 ++++++++++++++++--- .../cpp_namespace_enum_test.dart | 18 ++++++++ .../native_cpp_test/cpp_namespace_enum_test.h | 17 +++++++ .../cpp_namespace_enum_test_bindings.dart | 42 +++++++++++++++++ .../native_cpp_test/verify_bindings_test.dart | 3 ++ 6 files changed, 137 insertions(+), 23 deletions(-) diff --git a/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart index 35fec5324b..7c9387de33 100644 --- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart +++ b/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart @@ -55,18 +55,16 @@ EnumClass parseEnumDeclaration(clang_types.CXCursor cursor, Context context) { .where((c) => c.rawValue.startsWith('-')) .isNotEmpty; } else { - // For C++ enums declared inside one or more namespaces, [qualifiedName] is - // the fully-qualified name (e.g. `outer::inner::Color`). At global scope it - // equals [enumName]. + // For C++ enums declared inside one or more scopes, [qualifiedName] is the + // fully-qualified name (e.g. `outer::inner::Color` or + // `outer::Class::Color`). At global scope it equals [enumName]. final qualifiedName = _qualifiedName(usr, enumName); final decl = Declaration(usr: usr, originalName: qualifiedName); var dartName = config.enums.rename(decl); if (dartName.contains('::')) { - // The default `rename` returns the (qualified) original name unchanged, - // which isn't a valid Dart identifier. Flatten the namespace path into a - // single name joined by `$`, e.g. `outer::inner::Color` becomes - // `outer$inner$Color`. - dartName = _flattenNamespace(qualifiedName); + // C++ scope separators are not valid in Dart identifiers. Flatten any + // scopes left after user renaming, preserving the renamed prefix if any. + dartName = _flattenQualifiedName(dartName); } logger.fine('++++ Adding Enum: ${cursor.completeStringRepr()}'); enumClass = EnumClass( @@ -147,23 +145,25 @@ EnumClass parseEnumDeclaration(clang_types.CXCursor cursor, Context context) { /// Builds the fully-qualified C++ name of an enum from its [usr]. /// -/// A USR like `c:@N@outer@N@inner@E@Color` yields `outer::inner::Color`. At -/// global scope (no enclosing namespace) this just returns [leafName]. +/// A USR like `c:@N@outer@S@Palette@E@Tone` yields +/// `outer::Palette::Tone`. At global scope (no enclosing namespace or class) +/// this just returns [leafName]. String _qualifiedName(String usr, String leafName) { // After the `c:` prefix, USR tokens alternate between a single-char kind - // marker (`N` for namespace, `E` for enum, etc.) and its name. Collect the - // names of the enclosing namespaces by stepping over each marker/name pair. + // marker (`N` for namespace, `S` for class/struct, `E` for enum, etc.) and + // its name. Collect the names of the enclosing scopes by stepping over each + // marker/name pair. final parts = usr.split('@'); - final namespaces = []; + final scopes = []; for (var i = 1; i + 1 < parts.length; i += 2) { - if (parts[i] == 'N') namespaces.add(parts[i + 1]); + if (parts[i] == 'N' || parts[i] == 'S') scopes.add(parts[i + 1]); } - if (namespaces.isEmpty) return leafName; - return [...namespaces, leafName].join('::'); + if (scopes.isEmpty) return leafName; + return [...scopes, leafName].join('::'); } /// Flattens a `::`-qualified C++ name into a single Dart identifier by joining /// the path segments with `$`, e.g. `outer::inner::Color` becomes /// `outer$inner$Color`. -String _flattenNamespace(String qualifiedName) => +String _flattenQualifiedName(String qualifiedName) => qualifiedName.split('::').where((s) => s.isNotEmpty).join(r'$'); diff --git a/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart b/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart index 36f773694a..41edc5d98d 100644 --- a/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart +++ b/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart @@ -33,8 +33,11 @@ Set parseTranslationUnit( case clang_types.CXCursorKind.CXCursor_FunctionDecl: bindings.addAll(parseFunctionDeclaration(context, cursor)); break; - case clang_types.CXCursorKind.CXCursor_StructDecl: case clang_types.CXCursorKind.CXCursor_UnionDecl: + case clang_types.CXCursorKind.CXCursor_StructDecl: + addToBindings(bindings, _getCodeGenTypeFromCursor(context, cursor)); + _visitRecordForEnums(context, cursor, bindings, headers); + break; case clang_types.CXCursorKind.CXCursor_EnumDecl: case clang_types.CXCursorKind.CXCursor_ObjCInterfaceDecl: case clang_types.CXCursorKind.CXCursor_TypedefDecl: @@ -60,6 +63,7 @@ Set parseTranslationUnit( break; case clang_types.CXCursorKind.CXCursor_ClassDecl: addToBindings(bindings, parseClassDeclaration(context, cursor)); + _visitRecordForEnums(context, cursor, bindings, headers); break; case clang_types.CXCursorKind.CXCursor_Namespace: _visitNamespaceForEnums(context, cursor, bindings, headers); @@ -94,7 +98,7 @@ void addToBindings(Set bindings, Binding? b) { /// /// For now this is the only declaration kind generated from inside namespaces. // TODO: Dispatch ClassDecl, FunctionDecl, etc. here for full C++ namespace -// support. +// support. Class declarations are currently visited only to find nested enums. void _visitNamespaceForEnums( Context context, clang_types.CXCursor namespaceCursor, @@ -106,26 +110,56 @@ void _visitNamespaceForEnums( logger.fine('Skipping anonymous namespace.'); return; } - namespaceCursor.visitChildren((cursor) { + _visitChildrenForNestedEnums(context, namespaceCursor, bindings, headers); +} + +/// Recurses into a C++ record to surface enum declarations nested inside it. +void _visitRecordForEnums( + Context context, + clang_types.CXCursor recordCursor, + Set bindings, + Map headers, +) { + final logger = context.logger; + if (clang.clang_Cursor_isAnonymous(recordCursor) != 0) { + logger.fine('Skipping anonymous record.'); + return; + } + _visitChildrenForNestedEnums(context, recordCursor, bindings, headers); +} + +void _visitChildrenForNestedEnums( + Context context, + clang_types.CXCursor parentCursor, + Set bindings, + Map headers, +) { + final logger = context.logger; + parentCursor.visitChildren((cursor) { final file = cursor.sourceFileName(); if (file.isEmpty) return; if (!(headers[file] ??= context.config.headers.include(Uri.file(file)))) { logger.finest( - 'namespaceCursorVisitor:(not included) ${cursor.completeStringRepr()}', + 'nestedEnumCursorVisitor:(not included) ${cursor.completeStringRepr()}', ); return; } try { - logger.finest('namespaceCursorVisitor: ${cursor.completeStringRepr()}'); + logger.finest('nestedEnumCursorVisitor: ${cursor.completeStringRepr()}'); switch (clang.clang_getCursorKind(cursor)) { case clang_types.CXCursorKind.CXCursor_Namespace: _visitNamespaceForEnums(context, cursor, bindings, headers); break; + case clang_types.CXCursorKind.CXCursor_UnionDecl: + case clang_types.CXCursorKind.CXCursor_ClassDecl: + case clang_types.CXCursorKind.CXCursor_StructDecl: + _visitRecordForEnums(context, cursor, bindings, headers); + break; case clang_types.CXCursorKind.CXCursor_EnumDecl: addToBindings(bindings, _getCodeGenTypeFromCursor(context, cursor)); break; default: - logger.finer('namespaceCursorVisitor: CursorKind not implemented'); + logger.finer('nestedEnumCursorVisitor: CursorKind not implemented'); } } catch (e, s) { logger.severe(e); diff --git a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.dart b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.dart index 1b55bcc328..dfcd1e4fc3 100644 --- a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.dart +++ b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.dart @@ -9,8 +9,11 @@ import 'cpp_namespace_enum_test_bindings.dart'; void main() { group('CppNamespaceEnum', () { test('enums in namespaces are generated with flattened names', () { + expect(GlobalBox$State, isNotNull); + expect(GlobalPalette$Shade, isNotNull); expect(outer$Color, isNotNull); expect(outer$inner$Color, isNotNull); + expect(outer$Palette$Tone, isNotNull); expect(other$Color, isNotNull); }); @@ -25,6 +28,21 @@ void main() { expect(outer$inner$Color.magenta.value, 20); }); + test('scoped enum nested in a class inside a namespace', () { + expect(outer$Palette$Tone.light.value, 1); + expect(outer$Palette$Tone.dark.value, 2); + }); + + test('scoped enum nested in a class at global scope', () { + expect(GlobalPalette$Shade.dim.value, 7); + expect(GlobalPalette$Shade.bright.value, 8); + }); + + test('scoped enum nested in a struct at global scope', () { + expect(GlobalBox$State.closed.value, 30); + expect(GlobalBox$State.open.value, 31); + }); + test('leaf-name collision across namespaces is disambiguated', () { expect(other$Color.black.value, 100); expect(other$Color.white.value, 200); diff --git a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.h b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.h index fb4dabd46e..8c44858faa 100644 --- a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.h +++ b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test.h @@ -11,9 +11,26 @@ namespace inner { // `Color` with `outer::Color` and `other::Color`. enum class Color { cyan = 10, magenta = 20 }; } // namespace inner + +class Palette { + public: + // Scoped enum nested inside a class that is itself in a namespace. + enum class Tone { light = 1, dark = 2 }; +}; } // namespace outer namespace other { // Another `Color`, in a different namespace, to exercise leaf-name collisions. enum Color { black = 100, white = 200 }; } // namespace other + +class GlobalPalette { + public: + // Scoped enum nested inside a class at global scope. + enum class Shade { dim = 7, bright = 8 }; +}; + +struct GlobalBox { + // Scoped enum nested inside a struct at global scope. + enum class State { closed = 30, open = 31 }; +}; diff --git a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test_bindings.dart b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test_bindings.dart index 231a828daa..872b64e22f 100644 --- a/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test_bindings.dart +++ b/pkgs/ffigen/test/native_cpp_test/cpp_namespace_enum_test_bindings.dart @@ -4,6 +4,34 @@ // ignore_for_file: type=lint, unused_import, unused_element, deprecated_member_use_from_same_package import 'dart:ffi' as ffi; +enum GlobalBox$State { + closed(30), + open(31); + + final int value; + const GlobalBox$State(this.value); + + static GlobalBox$State fromValue(int value) => switch (value) { + 30 => closed, + 31 => open, + _ => throw ArgumentError('Unknown value for GlobalBox\$State: $value'), + }; +} + +enum GlobalPalette$Shade { + dim(7), + bright(8); + + final int value; + const GlobalPalette$Shade(this.value); + + static GlobalPalette$Shade fromValue(int value) => switch (value) { + 7 => dim, + 8 => bright, + _ => throw ArgumentError('Unknown value for GlobalPalette\$Shade: $value'), + }; +} + enum other$Color { black(100), white(200); @@ -34,6 +62,20 @@ enum outer$Color { }; } +enum outer$Palette$Tone { + light(1), + dark(2); + + final int value; + const outer$Palette$Tone(this.value); + + static outer$Palette$Tone fromValue(int value) => switch (value) { + 1 => light, + 2 => dark, + _ => throw ArgumentError('Unknown value for outer\$Palette\$Tone: $value'), + }; +} + enum outer$inner$Color { cyan(10), magenta(20); 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 1de24b30ea..cd4db1e53d 100644 --- a/pkgs/ffigen/test/native_cpp_test/verify_bindings_test.dart +++ b/pkgs/ffigen/test/native_cpp_test/verify_bindings_test.dart @@ -59,8 +59,11 @@ void main() { compilerOptions: ['-x', 'c++'], ), enums: Enums.includeSet({ + 'GlobalBox::State', + 'GlobalPalette::Shade', 'outer::Color', 'outer::inner::Color', + 'outer::Palette::Tone', 'other::Color', }), ), From 30379b4c585a2504f51e1efa192db6026da2cbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Korde=C5=BE?= Date: Tue, 14 Jul 2026 07:56:23 +0200 Subject: [PATCH 3/5] Add support for nested structs and unions with flattened names in FFI bindings --- .../sub_parsers/compounddecl_parser.dart | 21 ++++- .../sub_parsers/enumdecl_parser.dart | 29 +------ .../translation_unit_parser.dart | 47 +++++++---- .../type_extractor/extractor.dart | 11 ++- pkgs/ffigen/lib/src/header_parser/utils.dart | 33 +++++++- .../cpp_scoped_struct_test.dart | 72 ++++++++++++++++ .../native_cpp_test/cpp_scoped_struct_test.h | 51 ++++++++++++ .../cpp_scoped_struct_test_bindings.dart | 82 +++++++++++++++++++ .../native_cpp_test/verify_bindings_test.dart | 20 +++++ 9 files changed, 315 insertions(+), 51 deletions(-) create mode 100644 pkgs/ffigen/test/native_cpp_test/cpp_scoped_struct_test.dart create mode 100644 pkgs/ffigen/test/native_cpp_test/cpp_scoped_struct_test.h create mode 100644 pkgs/ffigen/test/native_cpp_test/cpp_scoped_struct_test_bindings.dart diff --git a/pkgs/ffigen/lib/src/header_parser/sub_parsers/compounddecl_parser.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/compounddecl_parser.dart index 23e01f33b1..8aa05b3572 100644 --- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/compounddecl_parser.dart +++ b/pkgs/ffigen/lib/src/header_parser/sub_parsers/compounddecl_parser.dart @@ -136,7 +136,13 @@ Compound? _parseCompoundDeclaration( return null; } - final decl = Declaration(usr: usr, originalName: declName); + // For C++ compounds declared inside one or more scopes, [qualifiedName] is + // the fully-qualified name (e.g. `outer::Point` or `Outer::Inner`). At + // global scope it equals [declName]. + final qualifiedName = declName.isEmpty + ? declName + : qualifiedNameFromUsr(usr, declName); + final decl = Declaration(usr: usr, originalName: qualifiedName); final Compound compound; if (declName.isEmpty) { cursor = context.cursorIndex.getDefinition(cursor); @@ -155,12 +161,19 @@ Compound? _parseCompoundDeclaration( } else { cursor = context.cursorIndex.getDefinition(cursor); context.logger.fine( - '++++ Adding $className: Name: $declName, ${cursor.completeStringRepr()}', + '++++ Adding $className: Name: $qualifiedName, ' + '${cursor.completeStringRepr()}', ); + var dartName = configDecl.rename(decl); + if (dartName.contains('::')) { + // C++ scope separators are not valid in Dart identifiers. Flatten any + // scopes left after user renaming, preserving the renamed prefix if any. + dartName = flattenQualifiedName(dartName); + } compound = constructor( usr: usr, - originalName: declName, - name: configDecl.rename(decl), + originalName: qualifiedName, + name: dartName, dartDoc: getCursorDocComment( context, cursor, diff --git a/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart index 7c9387de33..1c6d8fbd73 100644 --- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart +++ b/pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart @@ -58,13 +58,13 @@ EnumClass parseEnumDeclaration(clang_types.CXCursor cursor, Context context) { // For C++ enums declared inside one or more scopes, [qualifiedName] is the // fully-qualified name (e.g. `outer::inner::Color` or // `outer::Class::Color`). At global scope it equals [enumName]. - final qualifiedName = _qualifiedName(usr, enumName); + final qualifiedName = qualifiedNameFromUsr(usr, enumName); final decl = Declaration(usr: usr, originalName: qualifiedName); var dartName = config.enums.rename(decl); if (dartName.contains('::')) { // C++ scope separators are not valid in Dart identifiers. Flatten any // scopes left after user renaming, preserving the renamed prefix if any. - dartName = _flattenQualifiedName(dartName); + dartName = flattenQualifiedName(dartName); } logger.fine('++++ Adding Enum: ${cursor.completeStringRepr()}'); enumClass = EnumClass( @@ -142,28 +142,3 @@ EnumClass parseEnumDeclaration(clang_types.CXCursor cursor, Context context) { apiAvailability: apiAvailability, ); } - -/// Builds the fully-qualified C++ name of an enum from its [usr]. -/// -/// A USR like `c:@N@outer@S@Palette@E@Tone` yields -/// `outer::Palette::Tone`. At global scope (no enclosing namespace or class) -/// this just returns [leafName]. -String _qualifiedName(String usr, String leafName) { - // After the `c:` prefix, USR tokens alternate between a single-char kind - // marker (`N` for namespace, `S` for class/struct, `E` for enum, etc.) and - // its name. Collect the names of the enclosing scopes by stepping over each - // marker/name pair. - final parts = usr.split('@'); - final scopes = []; - for (var i = 1; i + 1 < parts.length; i += 2) { - if (parts[i] == 'N' || parts[i] == 'S') scopes.add(parts[i + 1]); - } - if (scopes.isEmpty) return leafName; - return [...scopes, leafName].join('::'); -} - -/// Flattens a `::`-qualified C++ name into a single Dart identifier by joining -/// the path segments with `$`, e.g. `outer::inner::Color` becomes -/// `outer$inner$Color`. -String _flattenQualifiedName(String qualifiedName) => - qualifiedName.split('::').where((s) => s.isNotEmpty).join(r'$'); diff --git a/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart b/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart index 41edc5d98d..1afe50105a 100644 --- a/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart +++ b/pkgs/ffigen/lib/src/header_parser/translation_unit_parser.dart @@ -36,7 +36,7 @@ Set parseTranslationUnit( case clang_types.CXCursorKind.CXCursor_UnionDecl: case clang_types.CXCursorKind.CXCursor_StructDecl: addToBindings(bindings, _getCodeGenTypeFromCursor(context, cursor)); - _visitRecordForEnums(context, cursor, bindings, headers); + _visitRecordForNestedDecls(context, cursor, bindings, headers); break; case clang_types.CXCursorKind.CXCursor_EnumDecl: case clang_types.CXCursorKind.CXCursor_ObjCInterfaceDecl: @@ -63,10 +63,10 @@ Set parseTranslationUnit( break; case clang_types.CXCursorKind.CXCursor_ClassDecl: addToBindings(bindings, parseClassDeclaration(context, cursor)); - _visitRecordForEnums(context, cursor, bindings, headers); + _visitRecordForNestedDecls(context, cursor, bindings, headers); break; case clang_types.CXCursorKind.CXCursor_Namespace: - _visitNamespaceForEnums(context, cursor, bindings, headers); + _visitNamespaceForNestedDecls(context, cursor, bindings, headers); break; default: logger.finer('rootCursorVisitor: CursorKind not implemented'); @@ -94,12 +94,15 @@ void addToBindings(Set bindings, Binding? b) { } } -/// Recurses into a C++ namespace, surfacing only enum declarations. +/// Recurses into a C++ namespace, surfacing enum, struct and union +/// declarations. /// -/// For now this is the only declaration kind generated from inside namespaces. +/// For now these are the only declaration kinds generated from inside +/// namespaces. // TODO: Dispatch ClassDecl, FunctionDecl, etc. here for full C++ namespace -// support. Class declarations are currently visited only to find nested enums. -void _visitNamespaceForEnums( +// support. Class declarations are currently visited only to find nested +// enums, structs and unions. +void _visitNamespaceForNestedDecls( Context context, clang_types.CXCursor namespaceCursor, Set bindings, @@ -110,11 +113,12 @@ void _visitNamespaceForEnums( logger.fine('Skipping anonymous namespace.'); return; } - _visitChildrenForNestedEnums(context, namespaceCursor, bindings, headers); + _visitChildrenForNestedDecls(context, namespaceCursor, bindings, headers); } -/// Recurses into a C++ record to surface enum declarations nested inside it. -void _visitRecordForEnums( +/// Recurses into a C++ record to surface enum, struct and union declarations +/// nested inside it. +void _visitRecordForNestedDecls( Context context, clang_types.CXCursor recordCursor, Set bindings, @@ -125,10 +129,10 @@ void _visitRecordForEnums( logger.fine('Skipping anonymous record.'); return; } - _visitChildrenForNestedEnums(context, recordCursor, bindings, headers); + _visitChildrenForNestedDecls(context, recordCursor, bindings, headers); } -void _visitChildrenForNestedEnums( +void _visitChildrenForNestedDecls( Context context, clang_types.CXCursor parentCursor, Set bindings, @@ -140,26 +144,33 @@ void _visitChildrenForNestedEnums( if (file.isEmpty) return; if (!(headers[file] ??= context.config.headers.include(Uri.file(file)))) { logger.finest( - 'nestedEnumCursorVisitor:(not included) ${cursor.completeStringRepr()}', + 'nestedDeclCursorVisitor:(not included) ${cursor.completeStringRepr()}', ); return; } try { - logger.finest('nestedEnumCursorVisitor: ${cursor.completeStringRepr()}'); + logger.finest('nestedDeclCursorVisitor: ${cursor.completeStringRepr()}'); switch (clang.clang_getCursorKind(cursor)) { case clang_types.CXCursorKind.CXCursor_Namespace: - _visitNamespaceForEnums(context, cursor, bindings, headers); + _visitNamespaceForNestedDecls(context, cursor, bindings, headers); break; case clang_types.CXCursorKind.CXCursor_UnionDecl: - case clang_types.CXCursorKind.CXCursor_ClassDecl: case clang_types.CXCursorKind.CXCursor_StructDecl: - _visitRecordForEnums(context, cursor, bindings, headers); + // Anonymous records are handled as members of their parent record, + // not as top-level bindings. + if (clang.clang_Cursor_isAnonymous(cursor) == 0) { + addToBindings(bindings, _getCodeGenTypeFromCursor(context, cursor)); + } + _visitRecordForNestedDecls(context, cursor, bindings, headers); + break; + case clang_types.CXCursorKind.CXCursor_ClassDecl: + _visitRecordForNestedDecls(context, cursor, bindings, headers); break; case clang_types.CXCursorKind.CXCursor_EnumDecl: addToBindings(bindings, _getCodeGenTypeFromCursor(context, cursor)); break; default: - logger.finer('nestedEnumCursorVisitor: CursorKind not implemented'); + logger.finer('nestedDeclCursorVisitor: CursorKind not implemented'); } } catch (e, s) { logger.severe(e); 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 484b38eca0..08a0fa8958 100644 --- a/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart +++ b/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart @@ -130,9 +130,18 @@ Type getCodeGenType( return BooleanType(); case clang_types.CXTypeKind.CXType_Attributed: case clang_types.CXTypeKind.CXType_Unexposed: + // For attributed types, the modified type is the underlying type. Other + // unexposed types (e.g. a C++ type referenced through a + // using-declaration, like `std::uint16_t`) have no modified type; + // resolve those via their canonical type instead. + var innerCxType = clang.clang_Type_getModifiedType(cxtype); + if (innerCxType.kind == clang_types.CXTypeKind.CXType_Invalid) { + final canonical = clang.clang_getCanonicalType(cxtype); + if (canonical.kind != cxtype.kind) innerCxType = canonical; + } final innerType = getCodeGenType( context, - clang.clang_Type_getModifiedType(cxtype), + innerCxType, originalCursor: originalCursor, ); final isNullable = diff --git a/pkgs/ffigen/lib/src/header_parser/utils.dart b/pkgs/ffigen/lib/src/header_parser/utils.dart index b7aefa6a4a..da9fa63f11 100644 --- a/pkgs/ffigen/lib/src/header_parser/utils.dart +++ b/pkgs/ffigen/lib/src/header_parser/utils.dart @@ -110,7 +110,10 @@ extension CXCursorExt on clang_types.CXCursor { String usr() { var res = clang.clang_getCursorUSR(this).toStringAndDispose(); - assert(!res.contains(synthUsrChar)); + // Raw USRs must not collide with the synthesized `~