Skip to content

go: fix exported-resource handle bookkeeping and use the standard generated-code header - #1746

Merged
dicej merged 3 commits into
bytecodealliance:mainfrom
sideeffffect:go-own-handle-and-generated-header
Oct 15, 2026
Merged

dicej merged 3 commits into
bytecodealliance:mainfrom
sideeffffect:go-own-handle-and-generated-header

Conversation

@sideeffffect

Copy link
Copy Markdown
Contributor

Draft: three small fixes to the Go generator for exported resources and the file header. I found them while using wit-bindgen 0.61.1 (through componentize-go) in a real project, where I currently patch the generated code by hand.

The variant-case vs. record name clash I also hit (variant job-failure-policy { constant(job-failure-policy-constant) }) is already fixed on main by #1717 (#1703), so this PR does not touch it.

1. Drop() on an exported resource calls OnDrop twice

For a resource the component exports, resource.drop on an own handle runs the destructor synchronously, because the dropping instance implements the resource. The generated resourceDtor<X> already clears the handle, unpins the object and calls OnDrop. Drop() then unpinned again and called OnDrop a second time.

// before
func (self *X) Drop() {
	handle := self.handle
	if self.handle != 0 {
		self.handle = 0
		resourceDropX(handle) // runs resourceDtorX -> OnDrop()
		self.pinner.Unpin()
		self.OnDrop()          // second call
	}
}

// after
func (self *X) Drop() {
	handle := self.handle
	if handle != 0 {
		resourceDropX(handle) // runs resourceDtorX: handle = 0, Unpin, OnDrop
	}
}

2. <Resource>FromOwnHandle does not record the incoming handle

When an own handle of an exported resource comes back into the exporting component (for example consume: func(x: x)), the callee gets a new handle in its own table. XFromOwnHandle returned the object but kept the old self.handle that TakeHandle handed out earlier. So Drop() released a stale handle (possibly another object's handle), and the incoming handle was never dropped.

// before
func XFromOwnHandle(handle int32) *X {
	return (*X)(unsafe.Pointer(resourceRepX(handle)))
}

// after
func XFromOwnHandle(handle int32) *X {
	self := (*X)(unsafe.Pointer(resourceRepX(handle)))
	self.handle = handle
	return self
}

The existing resources runtime test (consume) does not catch this, because there the incoming handle happens to reuse the slot that was handed out before. Bugs 1 and 2 also hid each other in a counter-based test: the double OnDrop made up for the destructor that never ran.

3. Generated-code header does not match Go's convention

Go tools recognize generated files by a line matching ^// Code generated .* DO NOT EDIT\.$ (https://go.dev/s/generatedcode). The old header did not match, so gopls, linters and GitHub's diff view treated the bindings as hand-written.

// before
// Generated by `wit-bindgen` 0.62.0. DO NOT EDIT!
// after
// Code generated by wit-bindgen 0.62.0. DO NOT EDIT.

Stub files (--generate-stubs) keep their header, because they are meant to be edited.

Tests

  • New runtime test tests/runtime/resource-own-roundtrip (Go + Rust for both runner and test). It passes two own handles back at the same time, so they cannot both reuse the slot handed out before, and it checks the destructor count.
    • Without fix 2 (fix 1 kept): panic: 1 not equal to 2.
    • With fix 2 but without fix 1: panic: 4 not equal to 2.
    • With both fixes: passes.
  • New unit test tests::generated_files_have_go_generated_code_header in wit-bindgen-go. It fails with the old header.

Run locally with the patched Go 1.27.1 from CI:

  • cargo fmt --all -- --check, cargo clippy -p wit-bindgen-go --all-targets: clean
  • cargo test -p wit-bindgen-go: 1 passed
  • cargo run test --languages go tests/codegen ...: 218 passed
  • cargo run test --languages rust,go tests/runtime ... --skip dylibs: 240 compiled, 184 runtime compositions passed. The skipped rust/dylibs* tests need wasi-sdk, which I don't have locally. I did not run the C runtime tests for the same reason.

🤖 Generated with Claude Code

sideeffffect and others added 3 commits October 13, 2026 20:43
For a resource this component exports, `resource.drop` on an own handle
runs the destructor synchronously (the canonical ABI calls the dtor when
the dropping instance implements the resource). The generated
`resourceDtor<X>` already clears the handle, unpins the object and calls
`OnDrop`, but `Drop()` then unpinned again and called `OnDrop` a second
time. Rely on the destructor instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an own handle of an exported resource is passed back into the
component that exports it (e.g. `consume: func(x: x)`), the callee gets
a new handle in its own table. `<Resource>FromOwnHandle` returned the
object without recording that handle, so `self.handle` still held the
one `TakeHandle` handed out earlier. `Drop()` then released a stale
handle (possibly another object's) and the incoming handle was never
dropped. Record it.

The new `resource-own-roundtrip` runtime test passes two own handles
back at once, so they cannot both reuse the slot handed out before; it
fails without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go tools recognize generated files by a line matching
`^// Code generated .* DO NOT EDIT\.$` (https://go.dev/s/generatedcode).
The previous `// Generated by `wit-bindgen` X. DO NOT EDIT!` header did
not match, so linters, `gopls` and GitHub's diff view treated the
bindings as hand-written. Stub files keep their header since they are
meant to be edited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sideeffffect sideeffffect changed the title [WIP] go: fix exported-resource handle bookkeeping and use the standard generated-code header go: fix exported-resource handle bookkeeping and use the standard generated-code header Oct 13, 2026
@sideeffffect
sideeffffect marked this pull request as ready for review October 13, 2026 19:02
@sideeffffect

Copy link
Copy Markdown
Contributor Author

@asteurer could you have a look please? 🙏

@asteurer asteurer 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.

Thanks @sideeffffect! I'll make some time tomorrow or Friday to look this over.

@asteurer asteurer 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 good to me! Below is a quick summary. @dicej if nothing looks amiss, do you mind merging?

1. OnDrop called twice

Confirmed bug. Per the Canonical ABI, canon_resource_drop calls the resource's rt.dtor() method which results in self.OnDrop() being called twice.

2. Incoming handle not recorded

Confirmed bug. <Resource>FromOwnHandle will sometimes store an outdated handle.

3. Go header convention

Great catch!

@dicej dicej left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

@dicej
dicej added this pull request to the merge queue Oct 15, 2026
Merged via the queue into bytecodealliance:main with commit 48952a6 Oct 15, 2026
29 checks passed
@sideeffffect
sideeffffect deleted the go-own-handle-and-generated-header branch October 16, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants