Skip to content

Commit

Permalink
found a way to get rid of libc
Browse files Browse the repository at this point in the history
It turns out that using vec as a buffer works, and it is even more performant. And when rust-lang/rust#89292 will be merged, things will be even simpler (no pop from the buffer).
  • Loading branch information
Sergey Bargamon committed Oct 2, 2021
1 parent c9abef0 commit 90913dd
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 47 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ edition = "2018"
objc = "0.2"
objc_id = "0.1"
objc-foundation = "0.1"
libc = "0.2"

[dev-dependencies]
criterion = "0.3"
Expand Down
4 changes: 2 additions & 2 deletions benches/my_benchmark.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use criterion::{criterion_group, criterion_main, Criterion};
use foundation::{leak, no_leak};
use foundation::{leak, no_leak_vec};

fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("to string");
let str = &"aaa 🍺".repeat(100);
group.bench_with_input("old", str, |b, str| b.iter(|| leak(str)));
group.bench_with_input("new", str, |b, str| b.iter(|| no_leak(str)));
group.bench_with_input("new vec", str, |b, str| b.iter(|| no_leak_vec(str)));
group.finish()
}

Expand Down
48 changes: 28 additions & 20 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# This repo is about reproducible memory leak in objc_foundation`s INSstring.as_str().



## how to see a leak

```shell
Expand All @@ -21,7 +19,7 @@ pub fn leak(str: &str) {

## why it produces a leak?

INSString.as_str() internaly uses UTF8String property of NSString.
INSString.as_str() internally uses UTF8String property of NSString.
[Apple doc](https://developer.apple.com/documentation/foundation/nsstring/1411189-utf8string?language=objc)
says that the memory behind this pointer has a lifetime shorter than a lifetime of an NSString itself.
But apparently, this is not entirely true. At least, this statement is not valid for strings that contain
Expand All @@ -36,27 +34,32 @@ So in the end, the actual leak occurs not in INSString.as_str() but, I guess, in
Yes. NSString::getCString ([Apple doc](https://developer.apple.com/documentation/foundation/nsstring/1415702-getcstring)) and we can use it like this:

```rust
pub fn nsstring_to_rust_string(nsstring: Id<NSString>) -> String {
unsafe {
let string_size: usize = msg_send![nsstring, lengthOfBytesUsingEncoding: 4];
// + 1 is because getCString returns null terminated string
let buffer = libc::malloc(string_size + 1) as *mut c_char;
let is_success: bool = msg_send![nsstring, getCString:buffer maxLength:string_size+1 encoding:4];
if is_success {
// CString will take care of memory from now on
CString::from_raw(buffer).to_str().unwrap().to_owned()
} else {
// In case getCString failed there is no point in creating CString
// So we must free memory
libc::free(buffer as *mut c_void);
// Original NSString::as_str() swallows all the errors.
// Not sure if that is the correct approach, but we also don`t have errors here.
"".to_string()
pub fn convert_with_vec(nsstring: Id<NSString>) -> String {
let string_size: usize = unsafe { msg_send![nsstring, lengthOfBytesUsingEncoding: 4] };
let mut buffer: Vec<u8> = vec![0_u8; string_size + 1];
let is_success: bool = unsafe {
msg_send![nsstring, getCString:buffer.as_mut_ptr() maxLength:string_size+1 encoding:4]
};
if is_success {
// before from_vec_with_nul can be used /~https://github.com/rust-lang/rust/pull/89292
// nul termination from the buffer should be removed by hands
buffer.pop();

unsafe {
CString::from_vec_unchecked(buffer)
.to_str()
.unwrap()
.to_string()
}
} else {
// In case getCString failed there is no point in creating CString
// Original NSString::as_str() swallows all the errors.
// Not sure if that is the correct approach, but we also don`t have errors here.
"".to_string()
}
}
```
If you change in main.rs leak to no_leak and again will run again
If you change in main.rs leak to no_leak_vec and will run again
```shell
cargo instruments -t Allocations
```
Expand All @@ -67,6 +70,11 @@ cargo becnh
```
You will see that performance even better.

```shell
to string/old time: [12.855 us 12.961 us 13.071 us]
to string/new vec time: [10.477 us 10.586 us 10.699 us]
```


The only problem I see with this solution is that it has a different return type (String instead of &str).
If you know how to fix that, or any other idea on how to do things better - please let me know.
Expand Down
49 changes: 26 additions & 23 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
use objc::{msg_send, sel, sel_impl};
use objc_foundation::{INSString, NSString};
use objc_id::Id;
use std::ffi::{c_void, CString};
use std::os::raw::c_char;
use std::ffi::CString;

pub fn leak(str: &str) {
let nsstrig = NSString::from_str(str);
nsstrig.as_str();
}

pub fn no_leak(str: &str) {
pub fn no_leak_vec(str: &str) {
let nsstrig = NSString::from_str(str);
nsstring_to_rust_string(nsstrig);
convert_with_vec(nsstrig);
}

/**
Expand All @@ -26,24 +25,28 @@ getCString:
https://developer.apple.com/documentation/foundation/nsstring/1415702-getcstring
*/
pub fn nsstring_to_rust_string(nsstring: Id<NSString>) -> String {
unsafe {
let string_size: usize = msg_send![nsstring, lengthOfBytesUsingEncoding: 4];
// + 1 is because getCString returns null terminated string
let buffer = libc::malloc(string_size + 1) as *mut c_char;
let is_success: bool =
msg_send![nsstring, getCString:buffer maxLength:string_size+1 encoding:4];
if is_success {
// CString will take care of memory from now on
CString::from_raw(buffer).to_str().unwrap().to_owned()
} else {
// In case getCString failed there is no point in creating CString
// So we must free memory
libc::free(buffer as *mut c_void);
// Original NSString::as_str() swallows all the errors.
// Not sure if that is the correct approach, but we also don`t have errors here.
"".to_string()
pub fn convert_with_vec(nsstring: Id<NSString>) -> String {
let string_size: usize = unsafe { msg_send![nsstring, lengthOfBytesUsingEncoding: 4] };
let mut buffer: Vec<u8> = vec![0_u8; string_size + 1];
let is_success: bool = unsafe {
msg_send![nsstring, getCString:buffer.as_mut_ptr() maxLength:string_size+1 encoding:4]
};
if is_success {
// before from_vec_with_nul can be used /~https://github.com/rust-lang/rust/pull/89292
// nul termination from the buffer should be removed by hands
buffer.pop();

unsafe {
CString::from_vec_unchecked(buffer)
.to_str()
.unwrap()
.to_string()
}
} else {
// In case getCString failed there is no point in creating CString
// Original NSString::as_str() swallows all the errors.
// Not sure if that is the correct approach, but we also don`t have errors here.
"".to_string()
}
}

Expand All @@ -53,7 +56,7 @@ mod tests {

#[test]
fn it_works() {
let nsstrig = NSString::from_str("aaabbb🍺Ыض");
assert_eq!(nsstring_to_rust_string(nsstrig), "aaabbb🍺Ыض");
let text = "aaabbb🍺Ыض";
assert_eq!(convert_with_vec(NSString::from_str(text)), text);
}
}

0 comments on commit 90913dd

Please sign in to comment.