-
Notifications
You must be signed in to change notification settings - Fork 120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use libBlocksRuntime from swift-corelibs-libdispatch as the blocks runtime #293
Use libBlocksRuntime from swift-corelibs-libdispatch as the blocks runtime #293
Conversation
|
Furthermore, while the test suite passes again, Valgrind shows an invalid read for the reference count in one additional place:
But this happens with the embedded blocks runtime as well, because |
Fixed in 628b291 |
537753f
to
29ffe29
Compare
if (BLOCKS_RUNTIME_LIBRARY) | ||
set(CMAKE_REQUIRED_LIBRARIES ${BLOCKS_RUNTIME_LIBRARY}) | ||
check_symbol_exists(_Block_use_RR2 "Block_private.h" HAVE_BLOCK_USE_RR2) | ||
if (HAVE_BLOCK_USE_RR2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the comment, it sounds as if things are broken if we don't have that symbol? Shouldn't we fail the build in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes absolutely. If forgot to make it fail hard when I remembered how, uhm, quirky the objc_constructInstance
/objc_destructInstance
API is that the legacy hook function would need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At some point, I am pondering doing the thing Apple does of embedding the refcount and weak flags in the isa pointer. I believe GNUstep should now all be clean to that.
It probably doesn’t make sense to do on 32-bit platforms so it would be 64-bit only, but would give us a nice memory saving there and would make it easier to support those APIs, at least on 64-bit platforms.
arc.mm
Outdated
if ((Class)&_NSConcreteMallocBlock == cls || | ||
(Class)&_NSConcreteStackBlock == cls) | ||
(Class)&_NSConcreteStackBlock == cls || | ||
(Class)&_NSConcreteAutoBlock == cls || | ||
(Class)&_NSConcreteFinalizingBlock == cls | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is starting to be a fairly expensive check. It's probably a good idea to use one of the spare bits in the info
field of objc_class
as an is-block check. Then we just set it once and do a single bitfield test and below, rather than a load of compares. Probably also good to mark this as unlikely as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small complication here is that objc_create_block_classes_as_subclasses_of()
only gets called when a Foundation library sets it up explicitly with its root class, so I had to pre-initialise just the info field when the runtime loads.
btw: I'm assuming that stack blocks are always promoted to the heap before they are assigned to weak references. Is that an accurate assumption? (we need a couple of checks for _NSConcreteStackBlock
otherwise).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not sure. I vaguely remember being able to take a weak reference to a block and have it go away when the block scope ended, but I’m not completely sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I've looked into this a bit more and did some experimentation and I think it's safe the way it is right now. The point I was worried about was what happens in objc_storeWeak()
, where we don't do anything to tie the lifetime of a stack block to that of the weak reference. But since this is all happening under ARC, the compiler will always emit a call to objc_retainBlock()
if there is any chance that the block will escape the current scope – at least that's my reading of CodeGenFunction::EmitARCRetainBlock()
in LLVM.
arc.mm
Outdated
{ | ||
_Block_release(obj); | ||
return; | ||
} | ||
if (cls == static_cast<Class>(&_NSConcreteStackBlock)) | ||
if (cls == static_cast<void*>(&_NSConcreteStackBlock)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have the is-block info
flag, we could move this up inside the is-it-a-block check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 7edfecc.
#ifdef EMBEDDED_BLOCKS_RUNTIME | ||
return (self->reserved) > 0 ? block : 0; | ||
#else | ||
return (self->flags) & BLOCK_REFCOUNT_MASK ? block : 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this not need some atomic operation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've only moved this out from the main blocks_runtime.c
and added a variant for libBlocksRuntime. Doing this without atomic operations should be fine because the function is only ever called when the runtime holds the weak reference lock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to have a comment to that effect, since that's in a different file. Can we race something calling retain / release on the block, or can that never happen because we're in the dealloc path here and it's UB if someone releases / retains a block after the refcount becomes zero?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we race something calling retain / release on the block
Thanks for pointing that! As it turns out, we are indeed susceptible to race conditions in objc_loadWeakRetained()
even with the current implementation.
Assume thread A holds a weak reference to a block and thread B the last strong reference to it. If thread A wants to promote the weak reference to a strong one, we can have the following sequence of events:
- Thread A: Calls
objc_loadWeakRetained()
and obtains the weak reference lock.block_load_weak()
returns the block because the reference count is still 1. - Thread B: Relinquishes the last strong reference, causing
_Block_release()
to run. The reference count drops to 0, so that destructors run and then the thread blocks again waiting to obtain the weak reference lock in order to delete the weak references. - Thread A: At the end of
objc_loadWeakRetained()
_Block_copy()
increments the reference count back up to 1, and returns a strong reference to the block to the caller. - Thread B: Proceeds to remove weak references, and frees the memory allocated for the block.
- Thread A: Still has the strong reference, which now points to freed memory 💥.
You've already documented the solution to that problem quite well in retain_fast()
, and libBlocksRuntime exposes a function _Block_tryRetain()
just for that purpose… I'm now using that and have implemented it for our embedded runtime as well.
Maybe you can have another look at the changeset in 97261ba to see whether that makes sense now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, thanks.
2d53e63
to
7edfecc
Compare
The FreeBSD 13.2 error is probably due to 13.3 being released. |
That seems to be it. Bumping that to 13.3 makes the build pass again. I don't really understand why it now shows "Some checks haven't completed yet", where it seems to wait for the freebsd-13-2 CI run 🤔 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Can you squash it down for merge?
83ed2e5
to
97261ba
Compare
Happy to, but the "required statuses must pass before merging" thing with the freebsd-13-2 job is blocking the merge. That seems to be configured in the repository settings under Rules→Rulesets. |
I can override that if you force push a cleaned up history. |
1883c5b
to
0f9f8ef
Compare
0f9f8ef
to
f36c17b
Compare
@ngrewe Thank you for working on this. MSYS2 includes the apple/swift-corelibs-libdispatch package and libobjc2. In that scenario, would the recommendation be to build libobjc2 using the external blocks runtime? |
Since this is brand new, I don't feel very confident about the term "recommendation" yet. But generally speaking, being able to use a platform-supplied libdispatch/libBlocksRuntime package without patches in conjunction with libobjc2 is the express purpose of this feature. |
Fixes #199
This PR enables using libBlocksRuntime as the blocks runtime in libobjc2. The two important prerequisites are:
Block_use_RR2()
function that we need for weak reference integration. The olderBlock_use_RR
hook would require implementingobjc_destructInstance()
.INSTALL_PRIVATE_HEADERS=ON
, which distributions tend to do in their -dev packages.The major differences between our internal blocks runtime and the external one are the following:
_NSConcreteAutoBlock
, and_NSConcreteFinalizingBlock
for blocks promoted from the stack to the heap (either without or with attached dtors), which the ARC machinery must be aware of to call out to the block retain/release function appropriately. In theory, this could be switched off in libBlocksRuntime if it is explicitly compiled with Objective-C support, but I don't entirely understand how their build process is supposed to achieve this and additionally, I wager, the probability of packagers willing to implement that is close to nil.reserved
field in the block structure, libBlocksRuntime blocks use a part of theflags
field.Block_use_RR2()
function mentioned above.