Repository navigation
Description
Summary
When a header is parsed as C++ (-x c++ in compilerOptions) but cpp: class support is not configured, any function whose signature mentions a pointer to a class is dropped from the bindings:
[WARNING] Skipped Function 'my_handle_create', function has unsupported return type or parameter type.
A pointer to a class is exactly as expressible as a pointer to a struct — it should be emitted as an Opaque, which is what already happens for struct OtherHandle;. Only the keyword differs.
Reproduction
src/handle.h:
class MyHandle; // forward-declared, used only through pointers
struct OtherHandle; // same thing, spelled `struct`
class DefinedHandle { // defined in the TU, still only used through pointers
public:
virtual ~DefinedHandle() = default;
virtual int value() const = 0;
private:
int state_;
};
MyHandle* my_handle_create(void);
void my_handle_destroy(MyHandle* h);
OtherHandle* other_handle_create(void);
DefinedHandle* defined_handle_create(void);(In a real header these would sit in an extern "C" block; that is left out here because extern "C" is currently ignored entirely, which is a separate issue)
tool/ffigen.dart:
import 'dart:io';
import 'package:ffigen/ffigen.dart';
Future<void> main() async {
final root = Platform.script.resolve('../');
await FfiGenerator(
output: Output(dart: DartOutput(path: root.resolve('lib/bindings.dart'))),
input: Input(
entryPoints: [root.resolve('src/handle.h')],
compilerOptions: ['-x', 'c++', '-std=c++17'],
),
// No `cpp:` configured.
visitors: [
Visitor(
func: (node) => node.isIncluded = {
'my_handle_create',
'my_handle_destroy',
'other_handle_create',
'defined_handle_create',
}.contains(node.originalName),
),
],
).generate();
}Actual (main @ f914da8):
[WARNING] Skipped Function 'my_handle_create', function has unsupported return type or parameter type.
[WARNING] Skipped Function 'my_handle_destroy', function has unsupported return type or parameter type.
[WARNING] Skipped Function 'defined_handle_create', function has unsupported return type or parameter type.
@ffi.Native<ffi.Pointer<OtherHandle> Function()>()
external ffi.Pointer<OtherHandle> other_handle_create();
final class OtherHandle extends ffi.Opaque {}Expected: all four functions are generated, with MyHandle and DefinedHandle emitted as ffi.Opaque just like OtherHandle.
Cause
In _extractfromRecord (pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart), a CXCursor_ClassDecl is only handled by the C++ class parser, which is a no-op when config.cpp == null (and also returns null for anonymous or unavailable classes). Whatever it declines falls through to the UnimplementedType default instead of the StructDecl/UnionDecl fallback, so the enclosing function is removed by parseFunctionDeclaration.
Treating CXCursor_ClassDecl the same as CXCursor_StructDecl in that fallback fixes it: the class goes through importType / parseStructDeclaration and, since it is only ever referenced through pointers, ends up as an Opaque.
Activity
liamappelbe commentedon Aug 27, 2026
Yeah the
classvsstructkeyword is irrelevant. We discussed deciding whether something was a struct or a class based onclang_isPODTypeinstead (then we can do theconfig.cppcheck only for non-pod types). Not sure if that check was implemented or not.cc @Hassnaa9
jakobkordez commentedon Sep 1, 2026
Seems reasonable. I checked the code and the check is not implemented yet.
Let me know if you want me to open a PR for either the POD check or the opaque fix. I have them both on my repo.
Not sure if GSoC is over or not...
liamappelbe commentedon Sep 1, 2026
GSoC is officially over, but Hassnaa is continuing to contribute. I don't think she's planning to work on this bug any time soon though, so feel free to send a PR.