Skip to content

Commit 94d29fe

Browse files
vorporealoz-agent
andauthored
Migrate warp_core + warpui_extras macOS code to objc2 (#11668)
## Description Migrates the macOS code in `warp_core` and `warpui_extras` from the deprecated `objc`/`cocoa` crates to `objc2`/`objc2-foundation`. - **`warp_core`**: `channel/state.rs` bundle-identifier lookup now uses the typed `objc2_foundation::NSBundle` API instead of `msg_send!` (`macos.rs`/`paths.rs` were already on objc2). Drops the now-unused `cocoa`/`objc` deps. - **`warpui_extras`**: `user_preferences/user_defaults.rs` now uses `Retained<NSUserDefaults>` with the typed `NSUserDefaults` API (replacing `StrongPtr` + `msg_send!`); the `user_preferences` feature swaps `cocoa`/`objc` for `objc2`/`objc2-foundation`. Part of the macOS `objc` → `objc2` migration; the primary goal is memory safety. This is binding-layer-only with **no logical changes**: read/write/remove semantics, bundle-id handling, and ownership are preserved exactly (objc2 `Retained` Drop replaces the manual `StrongPtr` release 1:1). The `.expect()` on `initWithSuiteName:` is documented and provably unreachable for the real `{app_id}-{profile}` suite-name format. ## Testing - `cargo check`, `cargo clippy --all-targets -- -D warnings`, and `cargo nextest run` for `warp_core` + `warpui_extras` pass (43 tests; after merging latest `master`). - An independent reviewer audited the diff for no-logical-change (NSUserDefaults ownership mapping, None/empty handling, and the `.expect()` unreachability proof) — clean. - [x] Warp Agent Mode — created via Warp's AI Agent Mode. <!-- CHANGELOG-NONE --> Co-Authored-By: Oz <oz-agent@warp.dev> --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent 2196ce9 commit 94d29fe

5 files changed

Lines changed: 56 additions & 88 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/warp_core/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ chrono.workspace = true
6464
tokio.workspace = true
6565

6666
[target.'cfg(target_os = "macos")'.dependencies]
67-
cocoa.workspace = true
68-
objc.workspace = true
6967
objc2-foundation = { workspace = true, features = [
7068
"NSString",
7169
"NSURL",

crates/warp_core/src/channel/state.rs

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -421,26 +421,17 @@ fn app_id_from_bundle() -> Option<AppId> {
421421
// We skip this for tests, as the call to `mainBundle` can take 30+ms,
422422
// which is a significant portion of the total test runtime.
423423
#[cfg(all(target_os = "macos", not(feature = "test-util")))]
424-
#[allow(deprecated)]
425-
unsafe {
426-
use cocoa::base::{id, nil};
427-
use cocoa::foundation::NSBundle;
428-
use objc::{msg_send, sel, sel_impl};
429-
use warpui::platform::mac::utils::nsstring_as_str;
430-
431-
let bundle = id::mainBundle();
432-
if bundle != nil {
433-
let nsstring: id = msg_send![bundle, bundleIdentifier];
434-
if nsstring != nil {
435-
let app_id = nsstring_as_str(nsstring)
436-
.expect("bundle IDs should always be valid UTF-8 strings");
437-
438-
if !app_id.is_empty() {
439-
return Some(
440-
AppId::parse(app_id)
441-
.expect("macOS bundle identifier has an unexpected format"),
442-
);
443-
}
424+
{
425+
use objc2_foundation::NSBundle;
426+
427+
let bundle = NSBundle::mainBundle();
428+
if let Some(bundle_identifier) = bundle.bundleIdentifier() {
429+
let app_id = bundle_identifier.to_string();
430+
if !app_id.is_empty() {
431+
return Some(
432+
AppId::parse(&app_id)
433+
.expect("macOS bundle identifier has an unexpected format"),
434+
);
444435
}
445436
}
446437
}

crates/warpui_extras/Cargo.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ license.workspace = true
1010
default = ["secure_storage", "user_preferences", "user_preferences-file"]
1111
secure_storage = ["dep:security-framework", "dep:secret-service", "dep:ouroboros"]
1212
test-util = []
13-
user_preferences = ["dep:cocoa", "dep:objc", "dep:gloo-storage"]
13+
user_preferences = ["dep:objc2", "dep:objc2-foundation", "dep:gloo-storage"]
1414
user_preferences-file = ["user_preferences", "dep:serde", "dep:serde_json"]
1515
user_preferences-toml = ["user_preferences", "dep:serde", "dep:serde_json", "dep:toml_edit"]
1616

@@ -31,8 +31,11 @@ warpui_extras = { path = ".", features = ["test-util"] }
3131
tempfile.workspace = true
3232

3333
[target.'cfg(target_os = "macos")'.dependencies]
34-
cocoa = { workspace = true, optional = true }
35-
objc = { workspace = true, optional = true }
34+
objc2 = { workspace = true, optional = true }
35+
objc2-foundation = { workspace = true, optional = true, features = [
36+
"NSString",
37+
"NSUserDefaults",
38+
] }
3639
security-framework = { version = "2.0.0", optional = true }
3740

3841
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
Lines changed: 37 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
//! Implementation of the [`UserPreferences`] trait using macOS user defaults.
22
3-
#![allow(deprecated)]
4-
5-
use cocoa::base::{id, nil};
6-
use objc::rc::StrongPtr;
7-
use objc::{class, msg_send, sel, sel_impl};
3+
use objc2::rc::Retained;
4+
use objc2::runtime::AnyObject;
5+
use objc2::AnyThread;
6+
use objc2_foundation::{NSString, NSUserDefaults};
87

98
/// A user preferences store backed by macOS user defaults (`NSUserDefaults`).
109
pub struct UserDefaultsPreferencesStorage {
1110
/// A strong reference to the `NSUserDefaults` backing store.
12-
user_defaults: StrongPtr,
11+
user_defaults: Retained<NSUserDefaults>,
1312
}
1413

1514
impl UserDefaultsPreferencesStorage {
@@ -29,74 +28,53 @@ impl UserDefaultsPreferencesStorage {
2928
///
3029
/// If [`None`] is provided as the suite name, the standard user defaults
3130
/// will be used (namespaced based on the current application).
32-
fn user_defaults(suite_name: Option<String>) -> StrongPtr {
33-
unsafe {
34-
// Calling `[[NSUserDefaults alloc] initWithSuiteName]`` where the suite name is the
35-
// application's bundle ID (the default `data_domain` if `data_profile` is unset)
36-
// _should_ be equivalent to `[NSUserDefaults standardUserDefaults]`. However, in case
37-
// the two ever deviate, we explicitly use `standardUserDefaults` below. The Apple docs
38-
// also imply that `standardUserDefaults` is cached.
39-
if let Some(suite_name) = &suite_name {
40-
let defaults: id = msg_send![class!(NSUserDefaults), alloc];
41-
let suite_name = util::make_nsstring(suite_name);
31+
fn user_defaults(suite_name: Option<String>) -> Retained<NSUserDefaults> {
32+
// Calling `[[NSUserDefaults alloc] initWithSuiteName]`` where the suite name is the
33+
// application's bundle ID (the default `data_domain` if `data_profile` is unset)
34+
// _should_ be equivalent to `[NSUserDefaults standardUserDefaults]`. However, in case
35+
// the two ever deviate, we explicitly use `standardUserDefaults` below. The Apple docs
36+
// also imply that `standardUserDefaults` is cached.
37+
if let Some(suite_name) = &suite_name {
38+
let suite_name = NSString::from_str(suite_name);
4239

43-
StrongPtr::new(msg_send![defaults, initWithSuiteName: *suite_name])
44-
} else {
45-
StrongPtr::retain(msg_send![class!(NSUserDefaults), standardUserDefaults])
46-
}
40+
// `initWithSuiteName:` only returns nil when the suite name is a reserved domain
41+
// (NSGlobalDomain/NSArgumentDomain/NSRegistrationDomain) or the app's own bundle
42+
// identifier; our `{app_id}-{profile}` suite name is never either of those, so this
43+
// is unreachable in practice.
44+
NSUserDefaults::initWithSuiteName(NSUserDefaults::alloc(), Some(&suite_name)).expect(
45+
"initWithSuiteName: only returns nil for a reserved domain or the app's own bundle id",
46+
)
47+
} else {
48+
NSUserDefaults::standardUserDefaults()
4749
}
4850
}
4951
}
5052

5153
impl super::UserPreferences for UserDefaultsPreferencesStorage {
5254
fn write_value(&self, key: &str, value: String) -> Result<(), super::Error> {
53-
unsafe {
54-
let key = util::make_nsstring(key);
55-
let value = util::make_nsstring(&value);
55+
let key = NSString::from_str(key);
56+
let value = NSString::from_str(&value);
57+
let value: &AnyObject = &value;
5658

57-
let _: () = msg_send![*self.user_defaults, setObject: *value forKey: *key];
58-
Ok(())
59+
// `setObject:forKey:` stores an arbitrary object; the value and key are
60+
// both `NSString`s, which are valid property-list types.
61+
unsafe {
62+
self.user_defaults.setObject_forKey(Some(value), &key);
5963
}
64+
Ok(())
6065
}
6166

6267
fn read_value(&self, key: &str) -> Result<Option<String>, super::Error> {
63-
unsafe {
64-
let key = util::make_nsstring(key);
65-
let value: id = msg_send![*self.user_defaults, stringForKey: *key];
66-
if value != nil {
67-
Ok(Some(
68-
warpui::platform::mac::utils::nsstring_as_str(value)?.to_owned(),
69-
))
70-
} else {
71-
Ok(None)
72-
}
68+
let key = NSString::from_str(key);
69+
match self.user_defaults.stringForKey(&key) {
70+
Some(value) => Ok(Some(value.to_string())),
71+
None => Ok(None),
7372
}
7473
}
7574

7675
fn remove_value(&self, key: &str) -> Result<(), super::Error> {
77-
unsafe {
78-
let key = util::make_nsstring(key);
79-
let _: () = msg_send![*self.user_defaults, removeObjectForKey: *key];
80-
Ok(())
81-
}
82-
}
83-
}
84-
85-
mod util {
86-
use cocoa::base::nil;
87-
use cocoa::foundation::NSString;
88-
use objc::rc::StrongPtr;
89-
90-
/// Creates a new `NSString` from the given `&str`, wrapped in a
91-
/// [`StrongPtr`] so it is released when the `StrongPtr` is dropped.
92-
///
93-
/// **Important:** when passing the result to `msg_send!`, always
94-
/// dereference it (e.g. `msg_send![obj, foo: *nsstring]`). Passing the
95-
/// `StrongPtr` itself by value causes it to be moved into the
96-
/// `unsafe extern fn` call that `msg_send!` transmutes to, and Rust will
97-
/// not run the `StrongPtr`'s `Drop` glue after that call, leaking the
98-
/// underlying `NSString`.
99-
pub fn make_nsstring(value: &str) -> StrongPtr {
100-
unsafe { StrongPtr::new(NSString::alloc(nil).init_str(value)) }
76+
let key = NSString::from_str(key);
77+
self.user_defaults.removeObjectForKey(&key);
78+
Ok(())
10179
}
10280
}

0 commit comments

Comments
 (0)