-
Notifications
You must be signed in to change notification settings - Fork 36
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
Conversation
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
Please be more specific on where the memory leak is, how to reproduce it and why your changes are necessary to fix it. |
@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 The leak occurs in at least two places. 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 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
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())
}
}
} I will gladly provide further explanations and/or fixes if needed. |
These seem like more fundamental issues with the 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. |
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. |
Well if you're willing to look to further look into this, feel free. But the current solution does not seem appropriate to me. |
I am still trying to fix this because it annoys me as hell :) Progress so far. Found a way to get rid of libc. I was kind of hoping that after the macOS update leak will go away, but no luck. |
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.
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()))
}
}
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
@@ -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; | |||
|
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.
Shouldn't be any whitespace here.
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.
Should I also remove whitespace from line 22?
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.
removed
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.
No you shouldn't.
src/osx_clipboard.rs
Outdated
use objc_foundation::{INSArray, INSObject, INSString}; | ||
use objc_foundation::{NSArray, NSDictionary, NSObject, NSString}; | ||
use objc_id::{Id, Owned}; | ||
|
||
use crate::common::*; |
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.
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.
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
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.
Thanks for the PR.
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