Repository navigation
Conversation
A Go thread has no autorelease pool, so an object autoreleased by a call from Go stayed allocated until its thread exited, which Go threads rarely do. Every characteristic write kept its NSData: 280 bytes for a 244-byte write, so a 2 MB firmware upload kept more than 2 MB. Every function that calls into Objective-C now starts with defer popPool(pushPool()): an autorelease pool, pushed on a locked thread and drained when the function returns, after it has copied any C string or bytes that point into the released objects. Draining exposed what the leak hid: - Nine arrays and buffers were autoreleased twice (scan, advertising, updateValue, and the service and characteristic setters), which over-releases them once a pool drains. - str_to_nsstring and str_to_nsuuid returned objects that nothing released; they now return them autoreleased, like the other helpers. - The peripherals the retrieve calls return were kept alive only by the leaked array; they are now retained, like the ones the delegate hands to Go. The tests check the C heap after 10000 calls, with nil receivers where a call would need a Bluetooth controller, and check that every function calling into Objective-C starts with the pool. CI now runs them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On arm64 macOS the allocator's in-use count moves in 16 KiB steps now and then, so a single run of 10000 calls could leave 16 KiB with nothing leaked, and CI failed at random. A leaking call leaves memory in every batch of calls, so the tests now run five batches of 2000 and fail only when every batch left more than 1 byte per call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Go thread has no autorelease pool, so an object autoreleased by a call from Go stays allocated until its thread exits, which Go threads rarely do. Every characteristic write kept its NSData: 280 bytes for a 244-byte write.
Every function that calls into Objective-C now starts with
defer popPool(pushPool()): an autorelease pool, pushed on a locked thread and drained when the function returns, after it has copied any C string or bytes that point into the released objects.btleplug fixed the same leak in its CoreBluetooth backend: deviceplug/btleplug#494 reported memory growing without bound while scanning, and deviceplug/btleplug@0250198 wraps each message on its CoreBluetooth thread in
objc_autoreleasePoolPush/Pop. cbgo has no thread of its own for calls from Go, so the pool goes around each call instead.Draining exposed what the leak hid:
updateValue, and the service and characteristic setters), which over-releases them once a pool drains.str_to_nsstringandstr_to_nsuuidreturned objects that nothing released. They now return them autoreleased, like the other helpers.The tests measure the C heap over 10000 calls, with nil receivers where a call would need a Bluetooth controller, and check that every function calling into Objective-C starts with the pool. CI now runs them.
Tested on macOS 15.8.1 against an ESP32-S3 peripheral through tinygo-org/bluetooth v0.16.0: connect by identifier, a read of every service, and seven complete uploads of 404 to 461 KB. go build, go vet and go test pass, and the package builds for ios/arm64.
🤖 Generated with Claude Code