-
-
Notifications
You must be signed in to change notification settings - Fork 51
Change JSClosure.release
to deinit
#33
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
Conversation
@carson-katri assigning you for review too, as I think if this is merged it will impact a lot of things in Tokamak. |
There’s |
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.
JSClosure.release
shouldn't be called at deinit
because it will break call of Swift function from JavaScript.
func observer(x: Int) {
let closure = JSClosure { _ in print(x) }
_ = JSObjectRef.global.document.object!.addEventListener!("visibilitychange", closure)
}
observe(x: 1)
observe(x: 2)
In your example, closure
will be deinitialized when returning from observer
function because call of addEventListener
won't increment Swift-side reference count. However, closure
can be called after deinit from JavaScript side and that call can't succeed because it's already released.
So users should call release
after un-registering event listener manually.
BTW, it may be helpful if JavaScriptKit warns when JSClosure is deinited before release
called
As @j-f1 said, after FinalizationRegistry will be available on all platforms, these kinds of manual memory management won't be necessary. |
That's exactly my point here. With this PR the call can't succeed and a user gets an error message about that, so the incorrect behavior becomes known. Without this PR the closure is deallocated anyway, the user doesn't get any error message, but the behavior is incorrect due to a single
I understand that, and it would be fantastic when that's the case. Unfortunately, we don't have it in all browsers, and even after (and if) it becomes available, we'll have to wait for a few years before new versions of Safari supporting it become widely adopted for this to work. The problem I described above is reproducible now and I hope we can at least let our users know about it. And it would also help us do the closure reference management audit in our code that depends on JavaScriptKit. |
What do you think about having |
At first, the lifecycle of closure is tied to the GC of JavaScript also, so I think closures should not be released on the Swift lifecycle. We should provide a way to manage to sync thier lifecycle system. That's
I think they are not equivalent behavior. With your proposal, the users will get error message when calling the closure from JavaScript. But with my proposal, the users will get error message when deallocating closure. The latter approach can tell error message to the user earlier. |
Oh, that makes sense, sorry for the confusion. Would |
Yes, that's exactly what I was thinking 😄 |
@kateinoigakukun that's done now. Uncovered an issue in the existing |
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
Consider this test case:
It will pass and it is reasonable to expect that: within
closureScope
theclosure
reference has its reference count decreased to zero as soon as the function returns. The second timeclosureScope
is called, the newclosure
instance is allocated at the same memory address as long as you don't allocate anything new on the heap between bothclosureScope
calls. That memory address is used as the result ofObjectIdentifier
. The side effect of this behavior manifests itself in this declaration in the body of theJSClosure
class:where in
JSClosure.init
the keys of this dictionary are taken fromObjectIdentifier
values:Currently the users of JavaScriptKit have to manually call
JSClosure.release()
to clean up this dictionary. This is error-prone and leads to bugs that are hard to diagnose. As far as I understand, the current test suite doesn't have access to a proper DOM environment to reproduce this fully, but consider this more complex case:You'd expect this to print
1
and then2
sequentially when the observer is triggered. That's not the case and you get2
printed twice since the second closure overwrites the first closure in thesharedFunctions
dictionary because both of them have the sameObjectIdentifier
value. The author of this code should have retained the closure for the expected lifetime of the observer to get differentObjectIdentifier
values, and then they should have calledrelease()
on those closures manually after that. They (well, in some cases that was me 😄) failed to do so, and JavaScriptKit didn't warn them about it.What I propose is changing
release()
todeinit
, while keeping its body. In the observer case both closures would be deallocated instantly, but at least a user would get a crash as soon as the observer triggered. This would notify them of the bug and give them a chance to fix it.Unfortunately, there's no way for us to be notified on the Swift side when closures are deallocated on the JavaScript side. JavaScript doesn't have finalizers, or destructors, or whatever you'd call it. So there's no way to fully fix this behavior and make it seamless. I'm afraid the crash at run time is the best I can propose so far, but at least it's better than a subtle bug that's hard to notice and hard to diagnose.