diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bcc9ab..3f1ddb7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,3 +20,7 @@ jobs: - name: vet run: go vet ./... + + # The tests need no Bluetooth controller. + - name: test + run: go test ./... diff --git a/att.go b/att.go index 689a242..c134997 100644 --- a/att.go +++ b/att.go @@ -48,24 +48,32 @@ type ATTRequest struct { // Central: https://developer.apple.com/documentation/corebluetooth/cbattrequest/1518995-central func (r ATTRequest) Central() Central { + defer popPool(pushPool()) + ptr := C.cb_atr_central(r.ptr) return Central{unsafe.Pointer(ptr)} } // Characteristic: https://developer.apple.com/documentation/corebluetooth/cbattrequest/1518716-characteristic func (r ATTRequest) Characteristic() Characteristic { + defer popPool(pushPool()) + ptr := C.cb_atr_characteristic(r.ptr) return Characteristic{unsafe.Pointer(ptr)} } // Value: https://developer.apple.com/documentation/corebluetooth/cbattrequest/1518795-value func (r ATTRequest) Value() []byte { + defer popPool(pushPool()) + ba := C.cb_atr_value(r.ptr) return byteArrToByteSlice(&ba) } // SetValue: https://developer.apple.com/documentation/corebluetooth/cbattrequest/1518795-value func (r ATTRequest) SetValue(v []byte) { + defer popPool(pushPool()) + ba := byteSliceToByteArr(v) defer C.free(unsafe.Pointer(ba.data)) @@ -74,5 +82,7 @@ func (r ATTRequest) SetValue(v []byte) { // Offset: https://developer.apple.com/documentation/corebluetooth/cbattrequest/1518857-offset func (r ATTRequest) Offset() int { + defer popPool(pushPool()) + return int(C.cb_atr_offset(r.ptr)) } diff --git a/bt.h b/bt.h index ce4ee45..1d8dc22 100644 --- a/bt.h +++ b/bt.h @@ -79,6 +79,8 @@ struct pmgr_restore_opts { bool bt_start(); void bt_stop(); void bt_init(); +void *bt_pool_push(); +void bt_pool_pop(void *pool); // util.m struct byte_arr nsdata_to_byte_arr(const NSData *nsdata); diff --git a/bt.m b/bt.m index 1de9797..49fc97d 100644 --- a/bt.m +++ b/bt.m @@ -58,3 +58,20 @@ bt_dlg = [[BTDlg alloc] init]; [bt_dlg retain]; } + +/** + * Pushes an autorelease pool on the calling thread. Go threads have none, so + * an object autoreleased by a call from Go stays allocated until bt_pool_pop + * drains the pool this returns. + */ +void * +bt_pool_push() +{ + return [[NSAutoreleasePool alloc] init]; +} + +void +bt_pool_pop(void *pool) +{ + [(NSAutoreleasePool *)pool drain]; +} diff --git a/central.go b/central.go index 9ea553e..d892ef9 100644 --- a/central.go +++ b/central.go @@ -15,11 +15,15 @@ type Central struct { // Identifier: https://developer.apple.com/documentation/corebluetooth/cbpeer/1620687-identifier func (c Central) Identifier() UUID { + defer popPool(pushPool()) + cstr := C.cb_peer_identifier(c.ptr) return MustParseUUID(C.GoString(cstr)) } // MaximumUpdateValueLength: https://developer.apple.com/documentation/corebluetooth/cbcentral/1408800-maximumupdatevaluelength func (c Central) MaximumUpdateValueLength() int { + defer popPool(pushPool()) + return int(C.cb_cent_maximum_update_len(c.ptr)) } diff --git a/centralmanager.go b/centralmanager.go index 800f0b5..5ef57c1 100644 --- a/centralmanager.go +++ b/centralmanager.go @@ -64,6 +64,8 @@ func findCentralManagerDlg(ptr unsafe.Pointer) CentralManagerDelegate { // NewCentralManager creates a central manager. Specify a nil `opts` value for // defaults. Don't forget to call `SetDelegate()` afterwards! func NewCentralManager(opts *ManagerOpts) CentralManager { + defer popPool(pushPool()) + if opts == nil { opts = &DfltManagerOpts } @@ -84,6 +86,8 @@ func NewCentralManager(opts *ManagerOpts) CentralManager { // SetDelegate configures a receiver for a central manager's asynchronous // callbacks. func (cm CentralManager) SetDelegate(d CentralManagerDelegate) { + defer popPool(pushPool()) + if d != nil { cmgrPtrMap.add(cm.ptr, d) } @@ -92,11 +96,15 @@ func (cm CentralManager) SetDelegate(d CentralManagerDelegate) { // State: https://developer.apple.com/documentation/corebluetooth/cbmanager/1648600-state func (cm CentralManager) State() ManagerState { + defer popPool(pushPool()) + return ManagerState(C.cb_cmgr_state(cm.ptr)) } // Connect: https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518766-connectperipheral func (cm CentralManager) Connect(prph Peripheral, opts *CentralManagerConnectOpts) { + defer popPool(pushPool()) + if opts == nil { opts = &DfltCentralManagerConnectOpts } @@ -115,11 +123,15 @@ func (cm CentralManager) Connect(prph Peripheral, opts *CentralManagerConnectOpt // CancelConnect: https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518952-cancelperipheralconnection func (cm CentralManager) CancelConnect(prph Peripheral) { + defer popPool(pushPool()) + C.cb_cmgr_cancel_connect(cm.ptr, prph.ptr) } // RetrieveConnectedPeripheralsWithServices: https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518924-retrieveconnectedperipheralswith func (cm CentralManager) RetrieveConnectedPeripheralsWithServices(uuids []UUID) []Peripheral { + defer popPool(pushPool()) + strs := uuidsToStrArr(uuids) defer freeStrArr(&strs) @@ -138,6 +150,8 @@ func (cm CentralManager) RetrieveConnectedPeripheralsWithServices(uuids []UUID) // RetrievePeripheralsWithIdentifiers: https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1519127-retrieveperipheralswithidentifie func (cm CentralManager) RetrievePeripheralsWithIdentifiers(uuids []UUID) []Peripheral { + defer popPool(pushPool()) + strs := uuidsToStrArr(uuids) defer freeStrArr(&strs) @@ -156,6 +170,8 @@ func (cm CentralManager) RetrievePeripheralsWithIdentifiers(uuids []UUID) []Peri // Scan: https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518986-scanforperipheralswithservices func (cm CentralManager) Scan(serviceUUIDs []UUID, opts *CentralManagerScanOpts) { + defer popPool(pushPool()) + arrSvcUUIDs := uuidsToStrArr(serviceUUIDs) defer freeStrArr(&arrSvcUUIDs) @@ -176,10 +192,14 @@ func (cm CentralManager) Scan(serviceUUIDs []UUID, opts *CentralManagerScanOpts) // StopScan: https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518984-stopscan func (cm CentralManager) StopScan() { + defer popPool(pushPool()) + C.cb_cmgr_stop_scan(cm.ptr) } // IsScanning: https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1620640-isscanning func (cm CentralManager) IsScanning() bool { + defer popPool(pushPool()) + return bool(C.cb_cmgr_is_scanning(cm.ptr)) } diff --git a/characteristic.go b/characteristic.go index f42d442..a1e1abf 100644 --- a/characteristic.go +++ b/characteristic.go @@ -39,24 +39,32 @@ type Characteristic struct { // UUID: https://developer.apple.com/documentation/corebluetooth/cbattribute/1620638-uuid func (c Characteristic) UUID() UUID { + defer popPool(pushPool()) + cstr := C.cb_chr_uuid(c.ptr) return MustParseUUID(C.GoString(cstr)) } // Service: https://developer.apple.com/documentation/corebluetooth/cbcharacteristic/1518728-service func (c Characteristic) Service() Service { + defer popPool(pushPool()) + ptr := C.cb_chr_service(c.ptr) return Service{ptr} } // Value: https://developer.apple.com/documentation/corebluetooth/cbcharacteristic/1518878-value func (c Characteristic) Value() []byte { + defer popPool(pushPool()) + ba := C.cb_chr_value(c.ptr) return byteArrToByteSlice(&ba) } // Descriptors: https://developer.apple.com/documentation/corebluetooth/cbcharacteristic/1518957-descriptors func (c Characteristic) Descriptors() []Descriptor { + defer popPool(pushPool()) + oa := C.cb_chr_descriptors(c.ptr) defer C.free(unsafe.Pointer(oa.objs)) @@ -71,10 +79,14 @@ func (c Characteristic) Descriptors() []Descriptor { // Properties: https://developer.apple.com/documentation/corebluetooth/cbcharacteristic/1519010-properties func (c Characteristic) Properties() CharacteristicProperties { + defer popPool(pushPool()) + return CharacteristicProperties(C.cb_chr_properties(c.ptr)) } // IsNotifying: https://developer.apple.com/documentation/corebluetooth/cbcharacteristic/1519057-isnotifying func (c Characteristic) IsNotifying() bool { + defer popPool(pushPool()) + return bool(C.cb_chr_is_notifying(c.ptr)) } diff --git a/cmgr.m b/cmgr.m index 40e0c76..7431494 100644 --- a/cmgr.m +++ b/cmgr.m @@ -57,7 +57,6 @@ const struct scan_opts *opts) { NSArray *arr_svc_uuids = strs_to_cbuuids(svc_uuids); - [arr_svc_uuids autorelease]; NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; [dict autorelease]; @@ -69,7 +68,6 @@ if (opts->sol_svc_uuids.count > 0) { NSArray *arr_sol_svc_uuids = strs_to_cbuuids(&opts->sol_svc_uuids); - [arr_sol_svc_uuids autorelease]; [dict setObject:arr_sol_svc_uuids forKey:CBCentralManagerScanOptionSolicitedServiceUUIDsKey]; @@ -150,8 +148,10 @@ void **objs = malloc([prphs count] * sizeof *objs); assert(objs != NULL); + // Go keeps these after the pool releases the array, so each one is + // retained like a peripheral the delegate hands to Go. for (int i = 0; i < [prphs count]; i++) { - objs[i] = [prphs objectAtIndex:i]; + objs[i] = [[prphs objectAtIndex:i] retain]; } return (struct obj_arr) { @@ -175,8 +175,10 @@ void **objs = malloc([prphs count] * sizeof *objs); assert(objs != NULL); + // Go keeps these after the pool releases the array, so each one is + // retained like a peripheral the delegate hands to Go. for (int i = 0; i < [prphs count]; i++) { - objs[i] = [prphs objectAtIndex:i]; + objs[i] = [[prphs objectAtIndex:i] retain]; } return (struct obj_arr) { diff --git a/cutil.go b/cutil.go index d153ac6..4abfadc 100644 --- a/cutil.go +++ b/cutil.go @@ -9,9 +9,29 @@ package cbgo import "C" import ( + "runtime" "unsafe" ) +// pushPool pushes an autorelease pool for a call into Objective-C, and locks +// the goroutine to its thread until popPool drains it: a pool belongs to the +// thread that pushed it. Every function that calls into Objective-C starts +// with +// +// defer popPool(pushPool()) +// +// so that what the call autoreleases is released when it returns, after the +// function has copied any C string or bytes that point into those objects. +func pushPool() unsafe.Pointer { + runtime.LockOSThread() + return C.bt_pool_push() +} + +func popPool(pool unsafe.Pointer) { + C.bt_pool_pop(pool) + runtime.UnlockOSThread() +} + func mallocArr(numElems int, elemSize uintptr) unsafe.Pointer { return C.malloc(C.size_t(numElems) * C.size_t(elemSize)) } diff --git a/descriptor.go b/descriptor.go index 740469e..5d1c1ac 100644 --- a/descriptor.go +++ b/descriptor.go @@ -15,18 +15,24 @@ type Descriptor struct { // UUID: https://developer.apple.com/documentation/corebluetooth/cbattribute/1620638-uuid func (d Descriptor) UUID() UUID { + defer popPool(pushPool()) + cstr := C.cb_dsc_uuid(d.ptr) return MustParseUUID(C.GoString(cstr)) } // Characteristic: https://developer.apple.com/documentation/corebluetooth/cbdescriptor/1519035-characteristic func (d Descriptor) Characteristic() Characteristic { + defer popPool(pushPool()) + ptr := C.cb_dsc_characteristic(d.ptr) return Characteristic{ptr} } // Value: https://developer.apple.com/documentation/corebluetooth/cbdescriptor/1518778-value func (d Descriptor) Value() []byte { + defer popPool(pushPool()) + ba := C.cb_dsc_value(d.ptr) return byteArrToByteSlice(&ba) } diff --git a/internal/heap/heap.go b/internal/heap/heap.go new file mode 100644 index 0000000..9e36a15 --- /dev/null +++ b/internal/heap/heap.go @@ -0,0 +1,21 @@ +// Package heap measures the C heap, for tests that check what a call into +// Objective-C leaves allocated. +package heap + +/* +#include + +static size_t +heap_in_use(void) +{ + malloc_statistics_t stats; + malloc_zone_statistics(NULL, &stats); + return stats.size_in_use; +} +*/ +import "C" + +// InUse returns the bytes allocated in every malloc zone of the process. +func InUse() int { + return int(C.heap_in_use()) +} diff --git a/l2capchannel.go b/l2capchannel.go index 21273a9..63b8587 100644 --- a/l2capchannel.go +++ b/l2capchannel.go @@ -69,6 +69,8 @@ type L2CAPChannel struct { // PSM returns the channel's Protocol/Service Multiplexer identifier. // PSM: https://developer.apple.com/documentation/corebluetooth/cbl2capchannel/2880155-psm func (ch L2CAPChannel) PSM() uint16 { + defer popPool(pushPool()) + return uint16(C.cb_l2cap_psm(ch.ptr)) } @@ -78,6 +80,8 @@ func (ch L2CAPChannel) PSM() uint16 { // UUID if the channel has no peer. // peer: https://developer.apple.com/documentation/corebluetooth/cbl2capchannel/peer func (ch L2CAPChannel) PeerIdentifier() UUID { + defer popPool(pushPool()) + peer := C.cb_l2cap_peer(ch.ptr) if peer == nil { return nil @@ -91,11 +95,15 @@ func (ch L2CAPChannel) PeerIdentifier() UUID { // InputStreamStatus returns the current state of the channel's input stream. func (ch L2CAPChannel) InputStreamStatus() StreamStatus { + defer popPool(pushPool()) + return StreamStatus(C.cb_l2cap_input_stream_status(ch.ptr)) } // OutputStreamStatus returns the current state of the channel's output stream. func (ch L2CAPChannel) OutputStreamStatus() StreamStatus { + defer popPool(pushPool()) + return StreamStatus(C.cb_l2cap_output_stream_status(ch.ptr)) } @@ -104,6 +112,8 @@ func (ch L2CAPChannel) OutputStreamStatus() StreamStatus { // currently available. It returns io.EOF once the remote end has closed the // stream. On failure it returns 0 and a non-nil error. func (ch L2CAPChannel) Read(buf []byte) (int, error) { + defer popPool(pushPool()) + if len(buf) == 0 { return 0, nil } @@ -127,6 +137,8 @@ func (ch L2CAPChannel) Read(buf []byte) (int, error) { // stream can no longer accept data. On failure it returns 0 and a non-nil // error. func (ch L2CAPChannel) Write(data []byte) (int, error) { + defer popPool(pushPool()) + if len(data) == 0 { return 0, nil } @@ -149,11 +161,15 @@ func (ch L2CAPChannel) Write(data []byte) (int, error) { // HasBytesAvailable returns true if the input stream has bytes available to read. func (ch L2CAPChannel) HasBytesAvailable() bool { + defer popPool(pushPool()) + return bool(C.cb_l2cap_has_bytes_available(ch.ptr)) } // HasSpaceAvailable returns true if the output stream has space available for writing. func (ch L2CAPChannel) HasSpaceAvailable() bool { + defer popPool(pushPool()) + return bool(C.cb_l2cap_has_space_available(ch.ptr)) } @@ -178,6 +194,8 @@ func (ch L2CAPChannel) SetEventHandler(fn func(event StreamEvent, input bool)) { // call more than once, and the channel's other methods remain safe to call // afterwards. func (ch L2CAPChannel) Close() { + defer popPool(pushPool()) + l2capHandlers.del(ch.ptr) C.cb_l2cap_close(ch.ptr) } diff --git a/mutablecharacteristic.go b/mutablecharacteristic.go index 54841a0..e652700 100644 --- a/mutablecharacteristic.go +++ b/mutablecharacteristic.go @@ -15,6 +15,7 @@ type MutableCharacteristic struct { func NewMutableCharacteristic(uuid UUID, properties CharacteristicProperties, value []byte, permissions AttributePermissions) MutableCharacteristic { + defer popPool(pushPool()) cuuid := C.CString(uuid.String()) defer C.free(unsafe.Pointer(cuuid)) @@ -35,6 +36,8 @@ func (c MutableCharacteristic) Characteristic() Characteristic { // SetDescriptors: https://developer.apple.com/documentation/corebluetooth/cbmutablecharacteristic/1518827-descriptors func (c MutableCharacteristic) SetDescriptors(mdscs []MutableDescriptor) { + defer popPool(pushPool()) + dscs := mallocObjArr(len(mdscs)) defer C.free(unsafe.Pointer(dscs.objs)) @@ -47,6 +50,8 @@ func (c MutableCharacteristic) SetDescriptors(mdscs []MutableDescriptor) { // SetValue: https://developer.apple.com/documentation/corebluetooth/cbmutablecharacteristic/1519121-value func (c MutableCharacteristic) SetValue(val []byte) { + defer popPool(pushPool()) + ba := byteSliceToByteArr(val) defer C.free(unsafe.Pointer(ba.data)) diff --git a/mutabledescriptor.go b/mutabledescriptor.go index 542205b..173b7cc 100644 --- a/mutabledescriptor.go +++ b/mutabledescriptor.go @@ -14,6 +14,8 @@ type MutableDescriptor struct { } func NewMutableDescriptor(uuid UUID, value []byte) MutableDescriptor { + defer popPool(pushPool()) + cuuid := C.CString(uuid.String()) defer C.free(unsafe.Pointer(cuuid)) diff --git a/mutableservice.go b/mutableservice.go index 9e3d98e..e6024ca 100644 --- a/mutableservice.go +++ b/mutableservice.go @@ -14,6 +14,8 @@ type MutableService struct { } func NewMutableService(uuid UUID, primary bool) MutableService { + defer popPool(pushPool()) + cuuid := C.CString(uuid.String()) defer C.free(unsafe.Pointer(cuuid)) @@ -29,6 +31,8 @@ func (s MutableService) Service() Service { // SetCharacteristics: https://developer.apple.com/documentation/corebluetooth/cbmutableservice/1434317-characteristics func (s MutableService) SetCharacteristics(mchrs []MutableCharacteristic) { + defer popPool(pushPool()) + chrs := mallocObjArr(len(mchrs)) defer C.free(unsafe.Pointer(chrs.objs)) @@ -41,6 +45,8 @@ func (s MutableService) SetCharacteristics(mchrs []MutableCharacteristic) { // SetIncludedServices: https://developer.apple.com/documentation/corebluetooth/cbmutableservice/1434320-includedservices func (s MutableService) SetIncludedServices(msvcs []MutableService) { + defer popPool(pushPool()) + svcs := mallocObjArr(len(msvcs)) defer C.free(unsafe.Pointer(svcs.objs)) diff --git a/peripheral.go b/peripheral.go index 59e090f..deb9ff5 100644 --- a/peripheral.go +++ b/peripheral.go @@ -42,24 +42,32 @@ func addPeripheralDlg(ptr unsafe.Pointer, dlg PeripheralDelegate) { // Identifier: https://developer.apple.com/documentation/corebluetooth/cbpeer/1620687-identifier func (p Peripheral) Identifier() UUID { + defer popPool(pushPool()) + cstr := C.cb_peer_identifier(p.ptr) return MustParseUUID(C.GoString(cstr)) } // Name: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1519029-name func (p Peripheral) Name() string { + defer popPool(pushPool()) + cstr := C.cb_prph_name(p.ptr) return C.GoString(cstr) } // SetDelegate: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518730-delegate func (p Peripheral) SetDelegate(dlg PeripheralDelegate) { + defer popPool(pushPool()) + addPeripheralDlg(p.ptr, dlg) C.cb_prph_set_delegate(p.ptr, C.bool(dlg != nil)) } // DiscoverServices: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518706-discoverservices func (p Peripheral) DiscoverServices(svcUUIDs []UUID) { + defer popPool(pushPool()) + arr := uuidsToStrArr(svcUUIDs) defer freeStrArr(&arr) @@ -68,6 +76,8 @@ func (p Peripheral) DiscoverServices(svcUUIDs []UUID) { // DiscoverIncludedServices: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1519014-discoverincludedservices func (p Peripheral) DiscoverIncludedServices(svcUUIDs []UUID, svc Service) { + defer popPool(pushPool()) + arr := uuidsToStrArr(svcUUIDs) defer freeStrArr(&arr) @@ -76,6 +86,8 @@ func (p Peripheral) DiscoverIncludedServices(svcUUIDs []UUID, svc Service) { // Services: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518978-services func (p Peripheral) Services() []Service { + defer popPool(pushPool()) + oa := C.cb_prph_services(p.ptr) defer C.free(unsafe.Pointer(oa.objs)) @@ -90,6 +102,8 @@ func (p Peripheral) Services() []Service { // DiscoverCharacteristics: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518797-discovercharacteristics func (p Peripheral) DiscoverCharacteristics(chrUUIDs []UUID, svc Service) { + defer popPool(pushPool()) + arr := uuidsToStrArr(chrUUIDs) defer freeStrArr(&arr) @@ -98,21 +112,29 @@ func (p Peripheral) DiscoverCharacteristics(chrUUIDs []UUID, svc Service) { // DiscoverDescriptors: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1519070-discoverdescriptorsforcharacteri func (p Peripheral) DiscoverDescriptors(chr Characteristic) { + defer popPool(pushPool()) + C.cb_prph_discover_dscs(p.ptr, chr.ptr) } // ReadCharacteristic: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518759-readvalueforcharacteristic func (p Peripheral) ReadCharacteristic(chr Characteristic) { + defer popPool(pushPool()) + C.cb_prph_read_chr(p.ptr, chr.ptr) } // ReadDescriptor: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518789-readvaluefordescriptor func (p Peripheral) ReadDescriptor(dsc Descriptor) { + defer popPool(pushPool()) + C.cb_prph_read_dsc(p.ptr, dsc.ptr) } // WriteCharacteristic: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518747-writevalue func (p Peripheral) WriteCharacteristic(data []byte, chr Characteristic, withRsp bool) { + defer popPool(pushPool()) + ba := byteSliceToByteArr(data) defer C.free(unsafe.Pointer(ba.data)) @@ -121,6 +143,8 @@ func (p Peripheral) WriteCharacteristic(data []byte, chr Characteristic, withRsp // WriteDescriptor: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1519107-writevalue func (p Peripheral) WriteDescriptor(data []byte, dsc Descriptor) { + defer popPool(pushPool()) + ba := byteSliceToByteArr(data) defer C.free(unsafe.Pointer(ba.data)) @@ -129,31 +153,43 @@ func (p Peripheral) WriteDescriptor(data []byte, dsc Descriptor) { // MaximumWriteValueLength: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1620312-maximumwritevaluelengthfortype func (p Peripheral) MaximumWriteValueLength(withRsp bool) int { + defer popPool(pushPool()) + val := C.cb_prph_max_write_len(p.ptr, chrWriteType(withRsp)) return int(val) } // SetNotify: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518949-setnotifyvalue func (p Peripheral) SetNotify(enabled bool, chr Characteristic) { + defer popPool(pushPool()) + C.cb_prph_set_notify(p.ptr, C.bool(enabled), chr.ptr) } // PeripheralState: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1519113-state func (p Peripheral) State() PeripheralState { + defer popPool(pushPool()) + return PeripheralState(C.cb_prph_state(p.ptr)) } // CanSendWriteWithoutResponse: https://developer.apple.com/documentation/corebluetooth/cbperipheral/2891512-cansendwritewithoutresponse func (p Peripheral) CanSendWriteWithoutResponse() bool { + defer popPool(pushPool()) + return bool(C.cb_prph_can_send_write_without_rsp(p.ptr)) } // ReadRSSI: https://developer.apple.com/documentation/corebluetooth/cbperipheral/1519111-readrssi func (p Peripheral) ReadRSSI() { + defer popPool(pushPool()) + C.cb_prph_read_rssi(p.ptr) } // OpenL2CAPChannel: https://developer.apple.com/documentation/corebluetooth/cbperipheral/2880157-openl2capchannel func (p Peripheral) OpenL2CAPChannel(psm uint16) { + defer popPool(pushPool()) + C.cb_prph_open_l2cap_channel(p.ptr, C.uint16_t(psm)) } diff --git a/peripheralmanager.go b/peripheralmanager.go index 6c0c34c..81d494a 100644 --- a/peripheralmanager.go +++ b/peripheralmanager.go @@ -45,6 +45,8 @@ func findPeripheralManagerDlg(ptr unsafe.Pointer) PeripheralManagerDelegate { // NewPeripheralManager creates a peripheral manager. Specify a nil `opts` // value for defaults. Don't forget to call `SetDelegate()` afterwards! func NewPeripheralManager(opts *ManagerOpts) PeripheralManager { + defer popPool(pushPool()) + if opts == nil { opts = &DfltManagerOpts } @@ -65,6 +67,8 @@ func NewPeripheralManager(opts *ManagerOpts) PeripheralManager { // SetDelegate configures a receiver for a central manager's asynchronous // callbacks. func (pm PeripheralManager) SetDelegate(d PeripheralManagerDelegate) { + defer popPool(pushPool()) + if d != nil { pmgrPtrMap.add(pm.ptr, d) } @@ -73,26 +77,36 @@ func (pm PeripheralManager) SetDelegate(d PeripheralManagerDelegate) { // State: https://developer.apple.com/documentation/corebluetooth/cbmanager/1648600-state func (pm PeripheralManager) State() ManagerState { + defer popPool(pushPool()) + return ManagerState(C.cb_pmgr_state(pm.ptr)) } // AddService: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393255-addservice func (pm PeripheralManager) AddService(svc MutableService) { + defer popPool(pushPool()) + C.cb_pmgr_add_svc(pm.ptr, svc.ptr) } // RemoveService: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393287-removeservice func (pm PeripheralManager) RemoveService(svc MutableService) { + defer popPool(pushPool()) + C.cb_pmgr_remove_svc(pm.ptr, svc.ptr) } // RemoveAllServices: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393269-removeallservices func (pm PeripheralManager) RemoveAllServices() { + defer popPool(pushPool()) + C.cb_pmgr_remove_all_svcs(pm.ptr) } // StartAdvertising: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393252-startadvertising func (pm PeripheralManager) StartAdvertising(ad AdvData) error { + defer popPool(pushPool()) + err := C.struct_bt_error{} cad := C.struct_adv_data{} @@ -121,16 +135,22 @@ func (pm PeripheralManager) StartAdvertising(ad AdvData) error { // StopAdvertising: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393275-stopadvertising func (pm PeripheralManager) StopAdvertising() { + defer popPool(pushPool()) + C.cb_pmgr_stop_adv(pm.ptr) } // IsAdvertising: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393291-isadvertising func (pm PeripheralManager) IsAdvertising() bool { + defer popPool(pushPool()) + return bool(C.cb_pmgr_is_adv(pm.ptr)) } // UpdateValue: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393281-updatevalue func (pm PeripheralManager) UpdateValue(value []byte, chr Characteristic, centrals []Central) bool { + defer popPool(pushPool()) + ba := byteSliceToByteArr(value) defer C.free(unsafe.Pointer(ba.data)) @@ -146,20 +166,28 @@ func (pm PeripheralManager) UpdateValue(value []byte, chr Characteristic, centra // RespondToRequest: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393293-respondtorequest func (pm PeripheralManager) RespondToRequest(req ATTRequest, result ATTError) { + defer popPool(pushPool()) + C.cb_pmgr_respond_to_req(pm.ptr, req.ptr, C.int(result)) } // SetDesiredConnectionLatency: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393277-setdesiredconnectionlatency func (pm PeripheralManager) SetDesiredConnectionLatency(latency PeripheralManagerConnectionLatency, central Central) { + defer popPool(pushPool()) + C.cb_pmgr_set_conn_latency(pm.ptr, C.int(latency), central.ptr) } // PublishL2CAPChannel: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/2880158-publishl2capchannel func (pm PeripheralManager) PublishL2CAPChannel(encryption bool) { + defer popPool(pushPool()) + C.cb_pmgr_publish_l2cap_channel(pm.ptr, C.bool(encryption)) } // UnpublishL2CAPChannel: https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/2880156-unpublishl2capchannel func (pm PeripheralManager) UnpublishL2CAPChannel(psm uint16) { + defer popPool(pushPool()) + C.cb_pmgr_unpublish_l2cap_channel(pm.ptr, C.uint16_t(psm)) } diff --git a/pmgr.m b/pmgr.m index 2653b5c..8e13edc 100644 --- a/pmgr.m +++ b/pmgr.m @@ -78,13 +78,11 @@ // iBeacon data is mutually exclusing with the rest of the fields. if (ad->ibeacon_data.length > 0) { NSData *nsd = byte_arr_to_nsdata(&ad->ibeacon_data); - [nsd autorelease]; [dict setObject:nsd forKey:@"kCBAdvDataAppleBeaconKey"]; } else { if (ad->name != NULL) { NSString *nss = str_to_nsstring(ad->name); - [nss autorelease]; [dict setObject:nss forKey:CBAdvertisementDataLocalNameKey]; @@ -92,7 +90,6 @@ if (ad->svc_uuids.count > 0) { NSArray *arr_svc_uuids = strs_to_cbuuids(&ad->svc_uuids); - [arr_svc_uuids autorelease]; [dict setObject:arr_svc_uuids forKey:CBAdvertisementDataServiceUUIDsKey]; @@ -118,10 +115,7 @@ cb_pmgr_update_val(void *pmgr, const struct byte_arr *value, void *chr, const struct obj_arr *centrals) { NSData *nsd = byte_arr_to_nsdata(value); - [nsd autorelease]; - NSArray *nsa = obj_arr_to_nsarray(centrals); - [nsa autorelease]; return [(CBPeripheralManager *)pmgr updateValue:nsd forCharacteristic:chr @@ -161,7 +155,6 @@ cb_msvc_set_characteristics(void *msvc, const struct obj_arr *mchrs) { NSArray *nsa = obj_arr_to_nsarray(mchrs); - [nsa autorelease]; ((CBMutableService *)msvc).characteristics = nsa; } @@ -170,7 +163,6 @@ cb_msvc_set_included_services(void *msvc, const struct obj_arr *msvcs) { NSArray *nsa = obj_arr_to_nsarray(msvcs); - [nsa autorelease]; ((CBMutableService *)msvc).includedServices = nsa; } @@ -194,7 +186,6 @@ cb_mchr_set_descriptors(void *mchr, const struct obj_arr *mdscs) { NSArray *nsa = obj_arr_to_nsarray(mdscs); - [nsa autorelease]; ((CBMutableCharacteristic *)mchr).descriptors = nsa; } diff --git a/pool_test.go b/pool_test.go new file mode 100644 index 0000000..8359060 --- /dev/null +++ b/pool_test.go @@ -0,0 +1,215 @@ +package cbgo_test + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/tinygo-org/cbgo" + "github.com/tinygo-org/cbgo/internal/heap" +) + +// TestEveryCallRunsInAPool checks that every function calling into +// Objective-C starts with `defer popPool(pushPool())`. Without it, what the +// call autoreleases stays allocated for as long as its thread lives, and Go +// threads rarely exit. The tests below check representative calls on the C +// heap; this one keeps the rest, and any added later, from going without. +func TestEveryCallRunsInAPool(t *testing.T) { + names, err := filepath.Glob("*.go") + if err != nil { + t.Fatal(err) + } + fset := token.NewFileSet() + for _, name := range names { + if strings.HasSuffix(name, "_test.go") { + continue + } + f, err := parser.ParseFile(fset, name, nil, 0) + if err != nil { + t.Fatal(err) + } + for _, decl := range f.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Body == nil || fn.Name.Name == "pushPool" || fn.Name.Name == "popPool" { + continue + } + if callsObjC(fn.Body) && !startsWithPool(fn.Body) { + t.Errorf("%s: %s calls into Objective-C outside an autorelease pool", fset.Position(fn.Pos()), fn.Name.Name) + } + } + } +} + +// callsObjC reports whether body calls one of the package's C functions, all +// of which are named cb_ or bt_. +func callsObjC(body *ast.BlockStmt) bool { + found := false + ast.Inspect(body, func(n ast.Node) bool { + if call, ok := n.(*ast.CallExpr); ok { + if sel, ok := call.Fun.(*ast.SelectorExpr); ok { + if pkg, ok := sel.X.(*ast.Ident); ok && pkg.Name == "C" && + (strings.HasPrefix(sel.Sel.Name, "cb_") || strings.HasPrefix(sel.Sel.Name, "bt_")) { + found = true + } + } + } + return !found + }) + return found +} + +func startsWithPool(body *ast.BlockStmt) bool { + if len(body.List) == 0 { + return false + } + d, ok := body.List[0].(*ast.DeferStmt) + return ok && types.ExprString(d.Call) == "popPool(pushPool())" +} + +// A test repeats a call in batches to see what it leaves behind. +const ( + batches = 5 + calls = 2000 // per batch +) + +// releases fails the test if calling fn leaves objects on the C heap. An +// object left in an autorelease pool costs at least its 8-byte pool slot, so +// a call that leaks one leaves 8 bytes or more each time, in every batch. The +// allocator's own count also moves, now and then and in 16 KiB steps on +// arm64, but not in every batch: a test fails only when every batch left more +// than 1 byte per call. +func releases(t *testing.T, fn func()) { + t.Helper() + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + fn() // The first call fills caches that every later call shares. + least := -1 + for b := 0; b < batches; b++ { + before := heap.InUse() + for i := 0; i < calls; i++ { + fn() + } + if left := heap.InUse() - before; least < 0 || left < least { + least = left + } + } + if least > calls { + t.Errorf("each batch of %d calls left at least %d bytes on the C heap (%d per call)", calls, least, least/calls) + } +} + +// Messages to a nil object do nothing in Objective-C, so the zero values +// below run each wrapper's own work without a Bluetooth controller. + +func TestWriteCharacteristicReleasesValue(t *testing.T) { + value := make([]byte, 244) + releases(t, func() { + cbgo.Peripheral{}.WriteCharacteristic(value, cbgo.Characteristic{}, false) + }) +} + +// A 16-bit UUID is short enough to become a tagged-pointer NSString, which is +// never allocated, so the tests use 128-bit ones too. +var ( + uuid16 = cbgo.UUID16(0xfe40) + uuid128 = cbgo.MustParseUUID("00467768-6228-2272-4663-277478268000") +) + +func TestScanReleasesFilters(t *testing.T) { + releases(t, func() { + cbgo.CentralManager{}.Scan([]cbgo.UUID{uuid16, uuid128}, &cbgo.CentralManagerScanOpts{ + SolicitedServiceUUIDs: []cbgo.UUID{uuid16, uuid128}, + }) + }) +} + +func TestRetrievePeripheralsWithIdentifiersReleasesIdentifiers(t *testing.T) { + releases(t, func() { + cbgo.CentralManager{}.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid128}) + }) +} + +func TestRetrieveConnectedPeripheralsWithServicesReleasesServices(t *testing.T) { + releases(t, func() { + cbgo.CentralManager{}.RetrieveConnectedPeripheralsWithServices([]cbgo.UUID{uuid16, uuid128}) + }) +} + +func TestCharacteristicUUIDReleasesString(t *testing.T) { + chr := cbgo.NewMutableCharacteristic(uuid128, cbgo.CharacteristicPropertyRead, nil, cbgo.AttributePermissionsReadable).Characteristic() + releases(t, func() { + if got := chr.UUID(); got.String() != uuid128.String() { + t.Fatalf("UUID() = %s, want %s", got, uuid128) + } + }) +} + +func TestServiceUUIDReleasesString(t *testing.T) { + svc := cbgo.NewMutableService(uuid128, true).Service() + releases(t, func() { + if got := svc.UUID(); got.String() != uuid128.String() { + t.Fatalf("UUID() = %s, want %s", got, uuid128) + } + }) +} + +func TestStartAdvertisingReleasesData(t *testing.T) { + ad := cbgo.AdvData{LocalName: "A long local name", ServiceUUIDs: []cbgo.UUID{uuid16, uuid128}} + releases(t, func() { + cbgo.PeripheralManager{}.StartAdvertising(ad) + }) +} + +func TestStartAdvertisingReleasesBeacon(t *testing.T) { + ad := cbgo.AdvData{IBeaconData: make([]byte, 21)} + releases(t, func() { + cbgo.PeripheralManager{}.StartAdvertising(ad) + }) +} + +func TestUpdateValueReleasesValue(t *testing.T) { + value := make([]byte, 244) + releases(t, func() { + cbgo.PeripheralManager{}.UpdateValue(value, cbgo.Characteristic{}, nil) + }) +} + +func TestSetCharacteristicsReleasesArray(t *testing.T) { + svc := cbgo.NewMutableService(uuid128, true) + chr := cbgo.NewMutableCharacteristic(uuid128, cbgo.CharacteristicPropertyRead, nil, cbgo.AttributePermissionsReadable) + releases(t, func() { + svc.SetCharacteristics([]cbgo.MutableCharacteristic{chr}) + }) + if n := len(svc.Service().Characteristics()); n != 1 { + t.Errorf("the service has %d characteristics, want 1", n) + } +} + +func TestSetIncludedServicesReleasesArray(t *testing.T) { + svc := cbgo.NewMutableService(uuid128, true) + inc := cbgo.NewMutableService(uuid16, false) + releases(t, func() { + svc.SetIncludedServices([]cbgo.MutableService{inc}) + }) + if n := len(svc.Service().IncludedServices()); n != 1 { + t.Errorf("the service includes %d services, want 1", n) + } +} + +func TestSetDescriptorsReleasesArray(t *testing.T) { + chr := cbgo.NewMutableCharacteristic(uuid128, cbgo.CharacteristicPropertyRead, nil, cbgo.AttributePermissionsReadable) + format := cbgo.NewMutableDescriptor(cbgo.UUID16(0x2904), make([]byte, 7)) // Presentation Format takes bytes + releases(t, func() { + chr.SetDescriptors([]cbgo.MutableDescriptor{format}) + }) + if n := len(chr.Characteristic().Descriptors()); n != 1 { + t.Errorf("the characteristic has %d descriptors, want 1", n) + } +} diff --git a/service.go b/service.go index f4c3d7f..70a019c 100644 --- a/service.go +++ b/service.go @@ -15,23 +15,31 @@ type Service struct { // UUID: https://developer.apple.com/documentation/corebluetooth/cbattribute/1620638-uuid func (s Service) UUID() UUID { + defer popPool(pushPool()) + cstr := C.cb_svc_uuid(s.ptr) return MustParseUUID(C.GoString(cstr)) } // Peripheral: https://developer.apple.com/documentation/corebluetooth/cbservice/1434334-peripheral func (s Service) Peripheral() Peripheral { + defer popPool(pushPool()) + prphPtr := C.cb_svc_peripheral(s.ptr) return Peripheral{prphPtr} } // IsPrimary: https://developer.apple.com/documentation/corebluetooth/cbservice/1434326-isprimary func (s Service) IsPrimary() bool { + defer popPool(pushPool()) + return bool(C.cb_svc_is_primary(s.ptr)) } // Characteristics: https://developer.apple.com/documentation/corebluetooth/cbservice/1434319-characteristics func (s Service) Characteristics() []Characteristic { + defer popPool(pushPool()) + oa := C.cb_svc_characteristics(s.ptr) defer C.free(unsafe.Pointer(oa.objs)) @@ -46,6 +54,8 @@ func (s Service) Characteristics() []Characteristic { // IncludedServices: https://developer.apple.com/documentation/corebluetooth/cbservice/1434324-includedservices func (s Service) IncludedServices() []Service { + defer popPool(pushPool()) + oa := C.cb_svc_included_svcs(s.ptr) defer C.free(unsafe.Pointer(oa.objs)) diff --git a/util.m b/util.m index 6c5cef0..45b14cc 100644 --- a/util.m +++ b/util.m @@ -53,7 +53,8 @@ NSString * str_to_nsstring(const char *s) { - return [[NSString alloc] initWithCString:s encoding:NSUTF8StringEncoding]; + NSString *nss = [[NSString alloc] initWithCString:s encoding:NSUTF8StringEncoding]; + return [nss autorelease]; } struct bt_error @@ -74,7 +75,8 @@ str_to_nsuuid(const char *s) { NSString *nss = str_to_nsstring(s); - return [[NSUUID alloc] initWithUUIDString:nss]; + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:nss]; + return [uuid autorelease]; } CBUUID *