Skip to content

[ffigen] Add retainOwnership/releaseOwnership to C++ class bindings#3459

Open
Hassnaa9 wants to merge 2 commits into
dart-lang:mainfrom
Hassnaa9:cpp-ownership-management
Open

[ffigen] Add retainOwnership/releaseOwnership to C++ class bindings#3459
Hassnaa9 wants to merge 2 commits into
dart-lang:mainfrom
Hassnaa9:cpp-ownership-management

Conversation

@Hassnaa9

@Hassnaa9 Hassnaa9 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Adds two new methods to every generated C++ Dart wrapper class:

- retainOwnership([customFinalizer]): attaches a NativeFinalizer to
  this object, taking ownership of the underlying C++ pointer. An
  optional native function pointer allows specifying a custom
  deallocator (e.g. free instead of delete).
- releaseOwnership(): detaches the finalizer, transferring ownership
  responsibility to the caller.

These are the escape hatches described as Option 1 in the memory
management design doc. They allow developers to correctly handle raw
C++ pointer APIs whose ownership semantics cannot be inferred from
the function signature alone.

The dispose() method now delegates its detach call to releaseOwnership()
for consistency. A per-instance _activeFinalizer field tracks whether
the object currently owns its pointer (null = unowned).

Tests cover:
- releaseOwnership suppresses the GC-triggered finalizer
- retainOwnership after releaseOwnership re-enables the GC finalizer
- Both methods are idempotent
@liamappelbe liamappelbe self-requested a review July 8, 2026 04:42
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

PR Health

Breaking changes ✔️
Package Change Current Version New Version Needed Version Looking good?

This check can be disabled by tagging the PR with skip-breaking-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

@liamappelbe liamappelbe left a comment

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.

Looks pretty good overall. I think we can probably simplify a little bit though. You've got 3 fields in the wrapper object: _ptr, _isDisposed, and _activeFinalizer.

The states the object can be in are:

State _ptr _activeFinalizer _isDisposed
Alive and owned non-null non-null false
Alive and unowned non-null null false
Disposed invalid invalid true

I think that's it? Are there any states I missed?

If that's all the states, then we can probably get rid of _isDisposed and just set both _ptr and _activeFinalizer to null.

@@ -120,14 +120,48 @@ class $name implements $ffiPrefix.Finalizable {
s.write('''
bool _isDisposed = false;
static final _finalizer = $ffiPrefix.NativeFinalizer(

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.

For clarity, call this _defaultFinalizer

/// default `delete` finalizer, which is useful when the object was not
/// allocated with `new` (e.g. `malloc` or a custom allocator).
///
/// Has no effect if this object already owns the pointer.

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.

Maybe this should throw a StateError?

/// underlying C++ pointer. The caller becomes responsible for freeing
/// the memory.
///
/// Has no effect if this object does not own the pointer.

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 should probably also throw a StateError

_isDisposed = true;
_finalizer.detach(this);
releaseOwnership();
$deleteGlue(_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.

What if they used a custom finalizer instead of deleteGlue?

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.

I think I've addressed the issue, but I still feel there may be some improvements to make, especially around the test coverage. I'll think through a few more scenarios and update them if I find any gaps.

@Hassnaa9 Hassnaa9 requested a review from liamappelbe July 9, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants