使用 Rust 中的 Objective-C 时管理 cocoa 内存的正确方法
The correct way to manage cocoa memory when working with Objective-C from Rust
我正在努力解决一个与 cocoa 基础内存管理相关的问题。基本上我有一个用 Rust 编写的项目,我正在使用 cocoa-rs
和 objc-rs
与 Objective-C 进行交互。我熟悉 CoreFoundation 和 CocoaFoundation 中的内存管理(我已经阅读了文档中的相应文章)。当我使用 CoreFoundation 函数时我没有任何内存问题,但是当我使用 CocoaFoundation 相关的东西时我遇到了很多问题,似乎从 CocoaFoundation 获取任何对象都会泄漏内存。
这里是导致内存记忆的函数之一的简化版本:
pub fn enumerate_apps()-> Vec<Rc<AppInfo>> {
let mut apps_list = Vec::new();
unsafe {
let shared_workspace: *mut Object = msg_send![class("NSWorkspace"), sharedWorkspace];
let running_apps: *mut Object = msg_send![shared_workspace, runningApplications];
let apps_count = msg_send![running_apps, count];
for i in 0..apps_count {
let app: *mut Object = msg_send![running_apps, objectAtIndex:i];
// Those ones are not used at the moment, but I actually need them,
// I just removed all business logic to keep the example simple and compilable
// to demonstrate the problem.
let bundle_url: *mut Object = msg_send![app, bundleURL];
let app_bundle: *mut Object = msg_send![class("NSBundle"), bundleWithURL:bundle_url];
let info_dict: *mut Object = msg_send![app_bundle, infoDictionary];
apps_list.push(Rc::new(AppInfo {
pid: msg_send![app, processIdentifier],
}));
}
}
apps_list
}
我试图在循环中调用此函数以使内存泄漏可见:
fn main() {
loop {
for i in 0..200 {
enumerate_apps();
}
std::thread::sleep(std::time::Duration::from_millis(5000));
}
}
当我 运行 应用程序时,我可以看到它随着时间的推移消耗越来越多的内存。
我的问题是:为什么?在这样的 FFI 代码中管理内存的正确方法是什么?如果我 运行 XCode 中的相同代码,使用普通的 Objective-C,它工作正常并且似乎没有泄漏内存。嗯,之所以在XCode中没有泄露内存,是因为默认开启了ARC。据我所知,当我们以这种方式使用 Rust 中的 Objective-C 时,ARC 不会启用,所以基本上这意味着我们必须自己管理内存。注释包含 bundle_url
、app_bundle
、info_dict
的 3 行会产生内存泄漏消失的错觉(如果不注释它们,进程每 2 秒就会泄漏几兆字节的内存),但实际上内存仍然泄漏,但没有那么快。
我尝试了什么:
- 我试图在函数的开头创建一个
NSAutoreleasePool
,并在创建时为 bundle_url
和 app_bundle
调用 autorelease()
。没用,内存还是泄露了。
- 我尝试在
bundle_url
和 app_bundle
上手动调用 release()
,没有任何效果。
- 甚至尝试对它们调用
dealloc()
(我认为这是错误的方法),这也无助于解决我的问题。
我做错了什么吗?还是 objc-rs
中的错误(我想这不太可能,但谁知道呢)?
由于Objective-C ARC 未在objc-rs
/cocoa-rs
中实现,您需要遵循memory management rule,特别是对于此问题:您不得放弃不属于您的对象的所有权。也就是说,您不应该对任何返回的对象调用 autorelease()
、release()
或 dealloc()
。
你应该做的是create an NSAutoreleasePool inside the function,并且不要碰任何其他东西。池将在释放时释放所有这些对象。
pub fn enumerate_apps()-> Vec<Rc<AppInfo>> {
let mut apps_list = Vec::new();
unsafe {
let autoreleasePool: *mut Object = msg_send![class("NSAutoreleasePool"), new];
// ...
// all code unchanged
// ...
msg_send![autoreleasePool, release];
}
apps_list
}
为什么在bundle_url
/app_bundle
/info_dict
上调用autorelease()
/release()
/dealloc()
不能减少内存?因为泄漏内存的不仅仅是这些对象。最大的消耗是 running_apps
对象。
为什么显式调用 autorelease()
/release()
/dealloc()
是错误的?让我们回顾一下 ObjC 的内存管理规则,并将其与普通的 Rust 代码进行比较(我假设你知道 Rc<T>
类型是如何工作的):
您拥有您创建的任何对象 — 您使用名称以“alloc”、“new”、“copy”开头的方法创建对象,或“mutableCopy”
你可以这样想:
// Objective-C code:
NSMutableString* s = [NSMutableString new];
NSMutableString* t = [s mutableCopy];
// Similar to this in Rust:
let s: Rc<NSMutableString> = Rc::new(NSMutableString::new());
let t: Rc<NSMutableString> = Rc::new(s.mutableCopy());
您的代码从未调用任何以 "alloc"、"new"、"copy" 或 "mutableCopy" 开头的方法,因此您不拥有它们中的任何一个。所有 ObjC API 都遵循此命名约定。
您可以使用 retain.
获取对象的所有权
这类似于拥有一个对象a: Rc<T>
,然后通过调用b = Rc::clone(&a)
获得一个新的引用。现在 b
也 "owns" 通过引用计数的原始对象:
// Objective-C code:
NSMutableString* u = [t retain];
// Similar to this in Rust:
let u: Rc<NSMutableString> = Rc::clone(&u);
但是你从未调用过 retain
,所以你仍然没有任何对象。
当您不再需要它时,您必须放弃您拥有的对象的所有权 — 您通过向对象发送 release
消息或 autorelease
消息。
在 Rust 方面,发送 -release
消息等同于删除 Rc 对象。
// Objective-C code:
[u release];
// Similar to this in Rust:
drop(u);
-autorelease
将所有权转移到自动释放池。将找到最近分配的 NSAutoreleasePool,将对象的所有权移至该池中,我们只保留借用的引用(*).
// Objective-C code:
NSMutableString* v = [t autorelease];
// Similar to this in Rust:
let pool: &NSAutoreleasePool = find_top_autorelease_pool()?;
let v: &NSMutableString = pool.add_object(t);
// `t` is passed-by-value, so `pool` now owns `t`.
// `pool` returns a borrowed reference,
// so that we can still access the memory pointed to by `t`,
// but we no longer own it.
您不得放弃您不拥有的对象的所有权。
- 也就是说,你永远不能通过借用引用来删除内存。在 Rust 中这是不可能的,但是 Objective-C 没有借用检查器。
此外,调用 -dealloc
就像在 Rust 中通过 drop(*s)
显式调用析构函数一样。这绕过了引用计数机制和 is explicitly discouraged.
让我们回顾一下:
- None 你调用的方法(
sharedWorkspace
/runningApplications
/objectAtIndex:
/bundleURL
/bundleWithURL:
/infoDictionary
) 以 alloc
/new
/copy
/mutableCopy
. 开头
- 你从未打电话给
-retain
。
- 这意味着根据规则 1 和 2,你所有的东西都是借来的。
- 这意味着根据规则 4,你永远不应该调用
release()
或 autorelease()
。
对您不拥有的对象调用 -release
或 -autorelease
会导致双重释放。这可能会导致 SEGFAULT、无操作或任何未定义的行为。
如果我们不提供 NSAutoreleasePool,为什么程序会像筛子一样泄漏? runningApplications
/bundleWithURL:
方法确实会分配对象,但遵循 Cocoa 内存管理规则,它们会在内部调用 -autorelease
以确保您不会获得拥有的对象。但是如果我们不分配任何池,-autorelease
可以将所有权转移到任何地方,即那些自动释放的对象变得不属于任何人,没有人拥有释放它们的所有权,因此泄漏。
(*):这个类比并不完美,因为您可以通过 [[x autorelease] retain]
获得新的所有权。但是这个细节在这里并不重要。
我正在努力解决一个与 cocoa 基础内存管理相关的问题。基本上我有一个用 Rust 编写的项目,我正在使用 cocoa-rs
和 objc-rs
与 Objective-C 进行交互。我熟悉 CoreFoundation 和 CocoaFoundation 中的内存管理(我已经阅读了文档中的相应文章)。当我使用 CoreFoundation 函数时我没有任何内存问题,但是当我使用 CocoaFoundation 相关的东西时我遇到了很多问题,似乎从 CocoaFoundation 获取任何对象都会泄漏内存。
这里是导致内存记忆的函数之一的简化版本:
pub fn enumerate_apps()-> Vec<Rc<AppInfo>> {
let mut apps_list = Vec::new();
unsafe {
let shared_workspace: *mut Object = msg_send![class("NSWorkspace"), sharedWorkspace];
let running_apps: *mut Object = msg_send![shared_workspace, runningApplications];
let apps_count = msg_send![running_apps, count];
for i in 0..apps_count {
let app: *mut Object = msg_send![running_apps, objectAtIndex:i];
// Those ones are not used at the moment, but I actually need them,
// I just removed all business logic to keep the example simple and compilable
// to demonstrate the problem.
let bundle_url: *mut Object = msg_send![app, bundleURL];
let app_bundle: *mut Object = msg_send![class("NSBundle"), bundleWithURL:bundle_url];
let info_dict: *mut Object = msg_send![app_bundle, infoDictionary];
apps_list.push(Rc::new(AppInfo {
pid: msg_send![app, processIdentifier],
}));
}
}
apps_list
}
我试图在循环中调用此函数以使内存泄漏可见:
fn main() {
loop {
for i in 0..200 {
enumerate_apps();
}
std::thread::sleep(std::time::Duration::from_millis(5000));
}
}
当我 运行 应用程序时,我可以看到它随着时间的推移消耗越来越多的内存。
我的问题是:为什么?在这样的 FFI 代码中管理内存的正确方法是什么?如果我 运行 XCode 中的相同代码,使用普通的 Objective-C,它工作正常并且似乎没有泄漏内存。嗯,之所以在XCode中没有泄露内存,是因为默认开启了ARC。据我所知,当我们以这种方式使用 Rust 中的 Objective-C 时,ARC 不会启用,所以基本上这意味着我们必须自己管理内存。注释包含 bundle_url
、app_bundle
、info_dict
的 3 行会产生内存泄漏消失的错觉(如果不注释它们,进程每 2 秒就会泄漏几兆字节的内存),但实际上内存仍然泄漏,但没有那么快。
我尝试了什么:
- 我试图在函数的开头创建一个
NSAutoreleasePool
,并在创建时为bundle_url
和app_bundle
调用autorelease()
。没用,内存还是泄露了。 - 我尝试在
bundle_url
和app_bundle
上手动调用release()
,没有任何效果。 - 甚至尝试对它们调用
dealloc()
(我认为这是错误的方法),这也无助于解决我的问题。
我做错了什么吗?还是 objc-rs
中的错误(我想这不太可能,但谁知道呢)?
由于Objective-C ARC 未在objc-rs
/cocoa-rs
中实现,您需要遵循memory management rule,特别是对于此问题:您不得放弃不属于您的对象的所有权。也就是说,您不应该对任何返回的对象调用 autorelease()
、release()
或 dealloc()
。
你应该做的是create an NSAutoreleasePool inside the function,并且不要碰任何其他东西。池将在释放时释放所有这些对象。
pub fn enumerate_apps()-> Vec<Rc<AppInfo>> {
let mut apps_list = Vec::new();
unsafe {
let autoreleasePool: *mut Object = msg_send![class("NSAutoreleasePool"), new];
// ...
// all code unchanged
// ...
msg_send![autoreleasePool, release];
}
apps_list
}
为什么在bundle_url
/app_bundle
/info_dict
上调用autorelease()
/release()
/dealloc()
不能减少内存?因为泄漏内存的不仅仅是这些对象。最大的消耗是 running_apps
对象。
为什么显式调用 autorelease()
/release()
/dealloc()
是错误的?让我们回顾一下 ObjC 的内存管理规则,并将其与普通的 Rust 代码进行比较(我假设你知道 Rc<T>
类型是如何工作的):
您拥有您创建的任何对象 — 您使用名称以“alloc”、“new”、“copy”开头的方法创建对象,或“mutableCopy”
你可以这样想:
// Objective-C code: NSMutableString* s = [NSMutableString new]; NSMutableString* t = [s mutableCopy]; // Similar to this in Rust: let s: Rc<NSMutableString> = Rc::new(NSMutableString::new()); let t: Rc<NSMutableString> = Rc::new(s.mutableCopy());
您的代码从未调用任何以 "alloc"、"new"、"copy" 或 "mutableCopy" 开头的方法,因此您不拥有它们中的任何一个。所有 ObjC API 都遵循此命名约定。
您可以使用 retain.
获取对象的所有权这类似于拥有一个对象
a: Rc<T>
,然后通过调用b = Rc::clone(&a)
获得一个新的引用。现在b
也 "owns" 通过引用计数的原始对象:// Objective-C code: NSMutableString* u = [t retain]; // Similar to this in Rust: let u: Rc<NSMutableString> = Rc::clone(&u);
但是你从未调用过
retain
,所以你仍然没有任何对象。
当您不再需要它时,您必须放弃您拥有的对象的所有权 — 您通过向对象发送
release
消息或autorelease
消息。在 Rust 方面,发送
-release
消息等同于删除 Rc 对象。// Objective-C code: [u release]; // Similar to this in Rust: drop(u);
-autorelease
将所有权转移到自动释放池。将找到最近分配的 NSAutoreleasePool,将对象的所有权移至该池中,我们只保留借用的引用(*).// Objective-C code: NSMutableString* v = [t autorelease]; // Similar to this in Rust: let pool: &NSAutoreleasePool = find_top_autorelease_pool()?; let v: &NSMutableString = pool.add_object(t); // `t` is passed-by-value, so `pool` now owns `t`. // `pool` returns a borrowed reference, // so that we can still access the memory pointed to by `t`, // but we no longer own it.
您不得放弃您不拥有的对象的所有权。
- 也就是说,你永远不能通过借用引用来删除内存。在 Rust 中这是不可能的,但是 Objective-C 没有借用检查器。
此外,调用
-dealloc
就像在 Rust 中通过drop(*s)
显式调用析构函数一样。这绕过了引用计数机制和 is explicitly discouraged.
让我们回顾一下:
- None 你调用的方法(
sharedWorkspace
/runningApplications
/objectAtIndex:
/bundleURL
/bundleWithURL:
/infoDictionary
) 以alloc
/new
/copy
/mutableCopy
. 开头
- 你从未打电话给
-retain
。 - 这意味着根据规则 1 和 2,你所有的东西都是借来的。
- 这意味着根据规则 4,你永远不应该调用
release()
或autorelease()
。
对您不拥有的对象调用 -release
或 -autorelease
会导致双重释放。这可能会导致 SEGFAULT、无操作或任何未定义的行为。
如果我们不提供 NSAutoreleasePool,为什么程序会像筛子一样泄漏? runningApplications
/bundleWithURL:
方法确实会分配对象,但遵循 Cocoa 内存管理规则,它们会在内部调用 -autorelease
以确保您不会获得拥有的对象。但是如果我们不分配任何池,-autorelease
可以将所有权转移到任何地方,即那些自动释放的对象变得不属于任何人,没有人拥有释放它们的所有权,因此泄漏。
(*):这个类比并不完美,因为您可以通过 [[x autorelease] retain]
获得新的所有权。但是这个细节在这里并不重要。