Avoid dangling weak references to deallocating objects. (#200)
The previous checks for a deallocating object with negative reference count did not work because:
1. the sign bit has to be recreated as was happening in objc_delete_weak_refs()
2. there was no distinction between a saturated count and a negative reference count
refcount_max now indicates when the refcount has saturated and should no longer be mutated.
An underflow to -1 still maps to refcount_mask, allowing us to detect when an object is supposed to be deallocated.
Neither objc_release_fast_no_destroy_np() nor objc_retain_fast_np() mutate the refcount when it is one of those values, so the comment in objc_delete_weak_refs() was adjusted.
main
parent
c399119694
commit
73132a6c98
@ -0,0 +1,149 @@
|
||||
#include "Test.h"
|
||||
|
||||
void direct_saturation_test();
|
||||
|
||||
int main()
|
||||
{
|
||||
id obj = [Test new];
|
||||
assert(object_getRetainCount_np(obj) == 1);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
size_t count = object_getRetainCount_np(obj);
|
||||
id ret = objc_retain_fast_np(obj);
|
||||
assert(ret == obj);
|
||||
assert(object_getRetainCount_np(obj) == ++count);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
size_t count = object_getRetainCount_np(obj);
|
||||
BOOL destroy = objc_release_fast_no_destroy_np(obj);
|
||||
assert(destroy == NO);
|
||||
assert(object_getRetainCount_np(obj) == --count);
|
||||
}
|
||||
|
||||
{
|
||||
// Final release should prevent further retains and releases.
|
||||
assert(objc_release_fast_no_destroy_np(obj) == YES);
|
||||
assert(object_getRetainCount_np(obj) == 0);
|
||||
assert(objc_retain_fast_np(obj) == nil);
|
||||
assert(object_getRetainCount_np(obj) == 0);
|
||||
assert(objc_release_fast_no_destroy_np(obj) == NO);
|
||||
assert(object_getRetainCount_np(obj) == 0);
|
||||
}
|
||||
|
||||
object_dispose(obj);
|
||||
obj = [Test new];
|
||||
|
||||
{
|
||||
// Should not be able to delete weak refs until final release.
|
||||
id weak;
|
||||
assert(objc_initWeak(&weak, obj) == obj);
|
||||
assert(weak != nil);
|
||||
assert(objc_loadWeakRetained(&weak) == obj);
|
||||
assert(objc_release_fast_no_destroy_np(obj) == NO);
|
||||
// Assumes a return of NO means no effect on obj at all.
|
||||
assert(objc_delete_weak_refs(obj) == NO);
|
||||
assert(objc_loadWeakRetained(&weak) == obj);
|
||||
assert(objc_release_fast_no_destroy_np(obj) == NO);
|
||||
// This will also call objc_delete_weak_refs() and succeed.
|
||||
assert(objc_release_fast_no_destroy_np(obj) == YES);
|
||||
objc_destroyWeak(&weak);
|
||||
// Check what happens when the weak refs were already deleted.
|
||||
assert(objc_delete_weak_refs(obj) == YES);
|
||||
}
|
||||
|
||||
obj = object_dispose(obj);
|
||||
|
||||
direct_saturation_test();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// ----------------
|
||||
// This test has knowledge of the implementation details of the ARC
|
||||
// reference counting and may need modification if the details change.
|
||||
|
||||
const long refcount_shift = 1;
|
||||
const size_t weak_mask = ((size_t)1)<<((sizeof(size_t)*8)-refcount_shift);
|
||||
const size_t refcount_mask = ~weak_mask;
|
||||
const size_t refcount_max = refcount_mask - 1;
|
||||
|
||||
size_t get_refcount(id obj)
|
||||
{
|
||||
size_t *refCount = ((size_t*)obj) - 1;
|
||||
return *refCount & refcount_mask;
|
||||
}
|
||||
|
||||
void set_refcount(id obj, size_t count)
|
||||
{
|
||||
size_t *refCount = ((size_t*)obj) - 1;
|
||||
*refCount = (*refCount & weak_mask) | (count & refcount_mask);
|
||||
}
|
||||
|
||||
void direct_saturation_test()
|
||||
{
|
||||
{
|
||||
id obj = [Test new];
|
||||
// sanity check
|
||||
objc_retain_fast_np(obj);
|
||||
assert(object_getRetainCount_np(obj) == 2);
|
||||
assert(get_refcount(obj) == 1);
|
||||
|
||||
// Check the behaviour close to the maximum refcount.
|
||||
set_refcount(obj, refcount_max - 3);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max - 2);
|
||||
|
||||
assert(objc_retain_fast_np(obj) == obj);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max - 1);
|
||||
|
||||
id weak;
|
||||
assert(objc_initWeak(&weak, obj) == obj);
|
||||
assert(weak != nil);
|
||||
assert(objc_loadWeakRetained(&weak) == obj);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max);
|
||||
|
||||
// This retain should cause the count to saturate.
|
||||
assert(objc_retain_fast_np(obj) == obj);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max + 1);
|
||||
|
||||
// A saturated count is no longer affected by retains or releases.
|
||||
assert(objc_release_fast_no_destroy_np(obj) == NO);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max + 1);
|
||||
assert(objc_retain_fast_np(obj) == obj);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max + 1);
|
||||
|
||||
// Nor should any weak refs be deleted.
|
||||
assert(objc_delete_weak_refs(obj) == NO);
|
||||
assert(objc_loadWeakRetained(&weak) == obj);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max + 1);
|
||||
|
||||
// Cleanup (can skip this if it becomes an issue)
|
||||
objc_destroyWeak(&weak);
|
||||
set_refcount(obj, 0);
|
||||
objc_release_fast_no_destroy_np(obj);
|
||||
object_dispose(obj);
|
||||
}
|
||||
|
||||
{
|
||||
id obj = [Test new];
|
||||
set_refcount(obj, refcount_max - 2);
|
||||
assert(objc_retain_fast_np(obj) == obj);
|
||||
assert(objc_retain_fast_np(obj) == obj);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max + 1);
|
||||
|
||||
// Check we can init a weak ref to an object with a saturated count.
|
||||
id weak;
|
||||
assert(objc_initWeak(&weak, obj) == obj);
|
||||
assert(weak != nil);
|
||||
assert(objc_loadWeakRetained(&weak) == obj);
|
||||
assert(object_getRetainCount_np(obj) == refcount_max + 1);
|
||||
|
||||
// Cleanup (can skip this if it becomes an issue)
|
||||
objc_destroyWeak(&weak);
|
||||
set_refcount(obj, 0);
|
||||
objc_release_fast_no_destroy_np(obj);
|
||||
object_dispose(obj);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue