Closed amodm closed 1 year ago
Thanks for doing all of this, but I want to hold off merging just yet. Detailing all of this in an issue before actually implementing it would have been appreciated, since we already have a solution to the memory management problem implemented on the autorelease
branch that uses a different method than what you've done here. It's not part of main
because I'm not sure if it's actually sound, but I'd like to compare these two approaches in terms of performance, DX, and which actually works better.
I'll detail my solution in #8 and I'd love for you to compare it, it's very possible that my solution isn't sound considering that you seem to have greater knowledge of how Swift's memory management actually works.
Holding off is perfectly alright - merging the PR is your discretion, not my right. We can conclude about it in the discussion in #8.
After much discussion, I'm going to bite the bullet and go ahead with this implementation. While I don't like the API changes, the memory safety is worth the change. Thanks so much for doing all this, I definitely wouldn't have figured it out on my own!
Thanks @Brendonovich for prioritising safety. I agree that this API is uglier than the alternative we were discussing. I'm hoping this is something that we're able to macro on top off, to have a better DX.
Problem
The current model of managing memory is broken. As an example, replace the following snippet of
example/src/main.rs
:with the following (essentially just running the exact same code on a non-main thread):
This will break. I realised this when writing tests, and the exact code of the example wouldn't work (tests get executed on a non-main thread).
Reason
The reason is that in the current model, when we create an object on the Swift side and pass it on to Rust, we do not disconnect it from Swift's ARC. So when the Rust side
drop
s it (thus calling a.release()
on it viaSRObject
's drop implementation) , Swift's ARC still assumes this object needs to be dropped, eventually dropping it inautorelease
. This happens only when a separate thread is used.Fix
The recommended way to handle Swift & C-like languages interfacing, is sticking to the following rules (I've also mentioned corresponding changes done in this PR):
toRust()
function which does exactly that. EveryT: SRObject
return from Swift needs to be wrapped intoRust()
.Unmanaged..release()
to tell Swift to free it up. The current implementation ofSRObject::drop()
does that already, but had a bug, which has been fixed in this PR.SRString
object to Swift side, the only memory-safe way it can be sent, is as a reference (i.e.&value
, notvalue
). Let's take theget_file_thumbnail_base64(path: SRString)
of example. Ifpath
is anSRString
instead of&SRString
, Rust treats it as consumed in the function call. But the function here is anextern
function, so thedrop
onpath
never gets called - a memory leak!SRString
parameter, now needs to take in aptr: UnsafePointer<SRString>
instead, and useptr.pointee.to_string()
to get a Swift string.Cascading effects
Along with the above, this PR includes the following:
SRString
bindings - both from Rust to Swift & vice-versa. This is intest_string()
oftests/test_bindings.rs
. Similar tests can/should be created for other types, if this PR gets merged.leaks
command. Seetest_memory_leaks()
undertests/test_binding.rs
src-rs/test-build.rs
) and corresponding changes toCargo.toml
. This build script is a no-op unless specific test conditions are met, so does not create any overhead for users of this library.toRust()
and acceptSRString
as a reference on Swift side (i.e.UnsafePointer<SRString>)
I wish there was a non-API breaking change to do all of the above, but to the best of my knowledge, there isn't.