Skip to content
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

fix mac os memory leak #33

Merged
merged 13 commits into from
Oct 21, 2021
Merged

fix mac os memory leak #33

merged 13 commits into from
Oct 21, 2021

Conversation

Lurk
Copy link
Contributor

@Lurk Lurk commented Jul 28, 2021

TLDR;
I did not find an easy way to clean up NSArray, but there is a better option to get NSString from pasteboard - stringForType.

Next up. Even if send release to NSString, there are leftovers after conversion NSString to Rust string. Right now, conversion happens in INSString.as_str(), which internally uses NSString.UTF8String. UTF8String, according to Apple docs, is a pointer lifetime of which is less or equal to lifetime NSString itself. This is not true if the clipboard contains characters outside the ASCII range. That's why nsstring_to_rust_string function.

I tried to describe the full process in a blogpost

TLDR;
I did not find an easy way to clean up NSArray, but there is a better option to get NSString from pasteboard - stringForType.

Next up. Even if send release to NSString, there are leftovers after conversion NSString to Rust string. Right now, conversion happens in INSString.as_str(), which internally uses NSString.UTF8String. UTF8String, according to Apple docs, is a pointer lifetime of which is less or equal NSString. This is not true if the clipboard contains characters outside the ASCII range. That's why nsstring_to_rust_string function.

I tried to describe the full process here https://barhamon.com/post/rust_and_nsstring
@chrisduerr
Copy link
Member

Please be more specific on where the memory leak is, how to reproduce it and why your changes are necessary to fix it.

@Lurk
Copy link
Contributor Author

Lurk commented Jul 28, 2021

@chrisduerr Woah, that was fast responce :)

To reproduce memory leak on macOS

use copypasta::{ClipboardContext, ClipboardProvider};

fn main() {
    let mut ctx: ClipboardContext = ClipboardContext::new().unwrap();
    for _ in 0..20_000 {
        match ctx.get_contents() {
            Ok(str) => println!("{}", str.chars().count()),
            Err(_e) => println!("{:?}", _e),
        }
    }
}
cargo instruments -t Allocation

will produce a nice graph
full_glory

The leak occurs in at least two places.
First, and I am not sure about the actual reason here, NSArray of NSString we got here does not get released. If we send release to NSArray nothing happens but, if we send the release to NSString we got here

string_array[0]

memory leak drops ~3x

Since we use NSArray to get the first NSString out of it, we can use NSPasteboard::stringForType which will return NSString.

Now we can send release directly to NSString we are getting from NSPastebord.

now If we run

cargo instruments -t Allocation

again, we will see this
disappointment

Which is a huge 3x win but still.

Next leak, again I am not sure that I identified the source precisely, is that we use INSString.to_str() which internally uses NSString.UTF8String. Apple doc says

This C string is a pointer to a structure inside the string object, which may have a lifetime shorter than the string object and will certainly not have a longer lifetime. Therefore, you should copy the C string if it needs to be stored outside of the memory context in which you use this property.

And most of the time it is true, I mean this C string gets cleaned up, except when clipboard contains characters outside the ASCII range. In that case it lives forever.

Solution I come up with:

fn nsstring_to_rust_string(nsstring: *mut NSString) -> Result<String> {
    unsafe {
        let string_size: usize =
            msg_send![nsstring, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
        //we need +1 because getCString will return null terminated string
        let char_ptr = libc::malloc(string_size + 1);
        let res: bool = msg_send![nsstring, getCString:char_ptr  maxLength:string_size + 1 encoding:NSUTF8StringEncoding];
        if res {
            let c_string = CStr::from_ptr(char_ptr as *const i8);
            libc::free(char_ptr);
            Ok(c_string.to_string_lossy().into_owned())
        } else {
            libc::free(char_ptr);
            Err("Casting from NSString to Rust string has failed".into())
        }
    }
}

Now allocations is showing
final_result

I will gladly provide further explanations and/or fixes if needed.

@chrisduerr
Copy link
Member

These seem like more fundamental issues with the objc crates. I find it difficult to believe that your solution is the 'correct' approach here. I try to stay away from macOS as much as possible, but surely it's possible to use these types without leaking.

I'd maybe try and look at some other usage of the macos crates in winit maybe. But manual allocation with libc and malloc should surely be unnecessary.

@Lurk
Copy link
Contributor Author

Lurk commented Jul 29, 2021

Yeah, I discovered that leak in my first Rust project. Imagine how surprised I was :)

Anyway. I do not insist on this solution and will gladly use any other working solution. But every other clipboard crate I found in one way or another uses Avraham Weinstock implementation. And I looked hard, spent almost a month of evenings on it.

@chrisduerr
Copy link
Member

Well if you're willing to look to further look into this, feel free. But the current solution does not seem appropriate to me.

@Lurk
Copy link
Contributor Author

Lurk commented Oct 2, 2021

I am still trying to fix this because it annoys me as hell :)

Progress so far.

Found a way to get rid of libc.
Created repo with the reproducible example, fix, and benchmark (https://github.com/Lurk/nsstring_leak).
Created the issue in rust-objc-foundation (SSheldon/rust-objc-foundation#15)

I was kind of hoping that after the macOS update leak will go away, but no luck.

Sergey Bargamon added 5 commits October 2, 2021 22:36
This should be done at rust_objc_fondation.
Updating original pull request, just in case it need to be fixed right now
Since we get our string from pasteboard, we need to inform objc runtime that we finished using it. We can do it by ourselves by sending the release message or use Id::from_retained_ptr, and Id will release that NSString when it goes out of scope.
@Lurk
Copy link
Contributor Author

Lurk commented Oct 15, 2021

Good news, everyone.

in the last commit the fix reduced to this:

    fn get_contents(&mut self) -> Result<String> {
        let nsstring: *mut NSString =
            unsafe { msg_send![self.pasteboard, stringForType: NSPasteboardTypeString] };
        if nsstring.is_null() {
            Err("pasteboard#stringForType returned null".into())
        } else {
            let nsstring: Id<NSString> = unsafe { Id::from_retained_ptr(nsstring) };
            Ok(autoreleasepool(|| nsstring.as_str().to_owned()))
        }
    }
  • pasteboard allows to get string from clipboard so there is no need to get array
 let nsstring: *mut NSString = unsafe { msg_send![self.pasteboard, stringForType: NSPasteboardTypeString] };
let nsstring: Id<NSString> = unsafe { Id::from_retained_ptr(nsstring) };
Ok(autoreleasepool(|| nsstring.as_str().to_owned()))

I updated the tests and benchmarks here https://github.com/Lurk/nsstring_leak nothing is leaking and performance-wise it is fine.

src/osx_clipboard.rs Outdated Show resolved Hide resolved
src/osx_clipboard.rs Outdated Show resolved Hide resolved
src/osx_clipboard.rs Show resolved Hide resolved
@@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::mem::transmute;
use objc::rc::autoreleasepool;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be any whitespace here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I also remove whitespace from line 22?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you shouldn't.

use objc_foundation::{INSArray, INSObject, INSString};
use objc_foundation::{NSArray, NSDictionary, NSObject, NSString};
use objc_id::{Id, Owned};

use crate::common::*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whitespace before crate imports was intentional. Same as the whitespace after std. This way the alphabetic ordering doesn't get in the way of the logical ordering of relevance.

Please revert the removal of whitespace before the crate::common import.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

@chrisduerr chrisduerr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants