Skip to content

Commit 9b30f3b

Browse files
committed
Emit main thread requirements on protocols
1 parent c6f851c commit 9b30f3b

18 files changed

+261
-43
lines changed

crates/header-translator/src/cache.rs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,52 @@ use crate::Mutability;
1313
#[derive(Debug, PartialEq, Clone)]
1414
pub struct Cache<'a> {
1515
config: &'a Config,
16-
mainthreadonly_classes: BTreeSet<ItemIdentifier>,
16+
mainthreadonly_items: BTreeSet<ItemIdentifier>,
1717
}
1818

1919
impl<'a> Cache<'a> {
2020
pub fn new(output: &Output, config: &'a Config) -> Self {
21-
let mut mainthreadonly_classes = BTreeSet::new();
21+
let mut mainthreadonly_items = BTreeSet::new();
2222

2323
for library in output.libraries.values() {
2424
for file in library.files.values() {
2525
for stmt in file.stmts.iter() {
26-
if let Stmt::ClassDecl {
27-
id,
28-
mutability: Mutability::MainThreadOnly,
29-
..
30-
} = stmt
31-
{
32-
mainthreadonly_classes.insert(id.clone());
26+
match stmt {
27+
Stmt::ClassDecl {
28+
id,
29+
mutability: Mutability::MainThreadOnly,
30+
..
31+
} => {
32+
mainthreadonly_items.insert(id.clone());
33+
}
34+
Stmt::ProtocolDecl {
35+
id,
36+
required_mainthreadonly: true,
37+
..
38+
} => {
39+
mainthreadonly_items.insert(id.clone());
40+
}
41+
Stmt::ProtocolDecl {
42+
id,
43+
required_mainthreadonly: false,
44+
protocols,
45+
..
46+
} => {
47+
for protocol in protocols {
48+
if mainthreadonly_items.contains(protocol) {
49+
let _ = mainthreadonly_items.insert(id.clone());
50+
}
51+
}
52+
}
53+
_ => {}
3354
}
3455
}
3556
}
3657
}
3758

3859
Self {
3960
config,
40-
mainthreadonly_classes,
61+
mainthreadonly_items,
4162
}
4263
}
4364

@@ -100,7 +121,7 @@ impl<'a> Cache<'a> {
100121
for method in methods.iter_mut() {
101122
let mut result_type_contains_mainthreadonly: bool = false;
102123
method.result_type.visit_required_types(&mut |id| {
103-
if self.mainthreadonly_classes.contains(id) {
124+
if self.mainthreadonly_items.contains(id) {
104125
result_type_contains_mainthreadonly = true;
105126
}
106127
});
@@ -111,13 +132,13 @@ impl<'a> Cache<'a> {
111132
// include optional arguments like `Option<&NSView>` or
112133
// `&NSArray<NSView>`.
113134
argument.visit_toplevel_types(&mut |id| {
114-
if self.mainthreadonly_classes.contains(id) {
135+
if self.mainthreadonly_items.contains(id) {
115136
any_argument_contains_mainthreadonly = true;
116137
}
117138
});
118139
}
119140

120-
if self.mainthreadonly_classes.contains(id) {
141+
if self.mainthreadonly_items.contains(id) {
121142
if method.is_class {
122143
// Assume the method needs main thread if it's
123144
// declared on a main thread only class.

crates/header-translator/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ pub struct ProtocolData {
146146
#[serde(default)]
147147
pub skipped: bool,
148148
#[serde(default)]
149+
#[serde(rename = "requires-mainthreadonly")]
150+
pub requires_mainthreadonly: Option<bool>,
151+
#[serde(default)]
149152
pub methods: HashMap<String, MethodData>,
150153
}
151154

crates/header-translator/src/data/AppKit.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ data! {
3030
// `run` cannot be safe, the user must ensure there is no re-entrancy.
3131
}
3232

33+
class NSController: MainThreadOnly {}
34+
class NSObjectController: MainThreadOnly {}
35+
class NSArrayController: MainThreadOnly {}
36+
class NSDictionaryController: MainThreadOnly {}
37+
class NSTreeController: MainThreadOnly {}
38+
class NSUserDefaultsController: MainThreadOnly {}
39+
3340
// Documentation says:
3441
// > Color objects are immutable and thread-safe
3542
//
@@ -38,6 +45,8 @@ data! {
3845
unsafe -clear;
3946
}
4047

48+
class NSColorPicker: MainThreadOnly {}
49+
4150
class NSControl {
4251
unsafe -isEnabled;
4352
unsafe -setEnabled;
@@ -81,6 +90,8 @@ data! {
8190

8291
}
8392

93+
class NSFontManager: MainThreadOnly {}
94+
8495
// Documented Thread-Unsafe, but:
8596
// > One thread can create an NSImage object, draw to the image buffer,
8697
// > and pass it off to the main thread for drawing. The underlying image
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
data! {
2+
class AMWorkflowController: MainThreadOnly {}
23
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
data! {
2+
class OSAScriptController: MainThreadOnly {}
23
}

crates/header-translator/src/method.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -700,8 +700,7 @@ impl fmt::Display for Method {
700700
let param = handle_reserved(&crate::to_snake_case(param));
701701
write!(f, "{param}: {arg_ty}, ")?;
702702
}
703-
// FIXME: Skipping main thread only on protocols for now
704-
if self.mainthreadonly && !self.is_protocol {
703+
if self.mainthreadonly {
705704
write!(f, "mtm: MainThreadMarker")?;
706705
}
707706
write!(f, ")")?;

crates/header-translator/src/stmt.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,13 @@ impl Stmt {
698698

699699
verify_objc_decl(entity, context);
700700
let generics = parse_class_generics(entity, context);
701-
let protocols = parse_direct_protocols(entity, context);
701+
let mut protocols = parse_direct_protocols(entity, context);
702+
703+
let skipped_protocols = data
704+
.map(|data| data.skipped_protocols.clone())
705+
.unwrap_or_default();
706+
protocols.retain(|protocol| !skipped_protocols.contains(&protocol.name));
707+
702708
let (methods, designated_initializers) = parse_methods(
703709
entity,
704710
|name| ClassData::get_method_data(data, name),
@@ -825,7 +831,7 @@ impl Stmt {
825831
context,
826832
);
827833

828-
let (sendable, mainthreadonly) = parse_attributes(entity, context);
834+
let (sendable, mut mainthreadonly) = parse_attributes(entity, context);
829835

830836
if !designated_initializers.is_empty() {
831837
warn!(
@@ -834,6 +840,43 @@ impl Stmt {
834840
)
835841
}
836842

843+
// Set the protocol as main thread only if all methods are
844+
// main thread only.
845+
//
846+
// This is done to make the UI nicer when the user tries to
847+
// implement such traits.
848+
//
849+
// Note: This is a deviation from the headers, but I don't
850+
// see a way for this to be unsound? As an example, let's say
851+
// there is some Objective-C code that assumes it can create
852+
// an object which is not `MainThreadOnly`, and then sets it
853+
// as the application delegate.
854+
//
855+
// Rust code that later retrieves the delegate would assume
856+
// that the object is `MainThreadOnly`, and could use this
857+
// information to create `MainThreadMarker`; but they can
858+
// _already_ do that, since the only way to retrieve the
859+
// delegate in the first place would be through
860+
// `NSApplication`!
861+
if !methods.is_empty() && methods.iter().all(|method| method.mainthreadonly) {
862+
mainthreadonly = true;
863+
}
864+
865+
// Overwrite with config preference
866+
if let Some(data) = data
867+
.map(|data| data.requires_mainthreadonly)
868+
.unwrap_or_default()
869+
{
870+
if mainthreadonly == data {
871+
warn!(
872+
mainthreadonly,
873+
data,
874+
"set requires-mainthreadonly to the same value that it already has"
875+
);
876+
}
877+
mainthreadonly = data;
878+
}
879+
837880
vec![Self::ProtocolDecl {
838881
id,
839882
actual_name,
@@ -1630,7 +1673,7 @@ impl fmt::Display for Stmt {
16301673
protocols,
16311674
methods,
16321675
required_sendable: _,
1633-
required_mainthreadonly: _,
1676+
required_mainthreadonly,
16341677
} => {
16351678
writeln!(f, "extern_protocol!(")?;
16361679
write!(f, "{availability}")?;
@@ -1661,6 +1704,14 @@ impl fmt::Display for Stmt {
16611704
// }
16621705
// write!(f, "Send + Sync")?;
16631706
// }
1707+
if *required_mainthreadonly {
1708+
if protocols.is_empty() {
1709+
write!(f, ": ")?;
1710+
} else {
1711+
write!(f, "+ ")?;
1712+
}
1713+
write!(f, "IsMainThreadOnly")?;
1714+
}
16641715
writeln!(f, " {{")?;
16651716

16661717
for method in methods {

crates/header-translator/translation-config.toml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,20 @@ skipped = true
852852
[class.NSCollectionViewDiffableDataSource.methods.initWithCollectionView_itemProvider]
853853
skipped = true
854854

855+
# Requires `MainThreadOnly`, which I'm not sure is a good idea here?
856+
[class.NSCollectionViewDiffableDataSource]
857+
skipped-protocols = ["NSCollectionViewDataSource"]
858+
[class.NSManagedObjectContext]
859+
skipped-protocols = ["NSEditor", "NSEditorRegistration"]
860+
861+
# Most methods on these require MainThreadMarker anyhow
862+
[protocol.NSDraggingInfo]
863+
requires-mainthreadonly = true
864+
[protocol.NSBrowserDelegate]
865+
requires-mainthreadonly = true
866+
[protocol.NSSplitViewDelegate]
867+
requires-mainthreadonly = true
868+
855869
# Both protocols and classes
856870
[protocol.NSTextAttachmentCell]
857871
renamed = "NSTextAttachmentCellProtocol"
@@ -1614,7 +1628,7 @@ skipped-protocols = ["NSCopying", "NSMutableCopying"]
16141628
skipped-protocols = ["NSCopying", "NSMutableCopying"]
16151629

16161630
# Uses `NS_SWIFT_UI_ACTOR` on a static, which is hard to support.
1617-
#
1631+
#
16181632
# Will have to be a method that takes `MainThreadMarker`.
16191633
[static.NSApp]
16201634
skipped = true

crates/icrate/CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
3131
`MTLAccelerationStructureCommandEncoder` now take a nullable scratch buffer:
3232
- `refitAccelerationStructure_descriptor_destination_scratchBuffer_scratchBufferOffset`
3333
- `refitAccelerationStructure_descriptor_destination_scratchBuffer_scratchBufferOffset_options`
34-
* **BREAKING**: Marked UI-related types as `MainThreadOnly`. This means that
35-
they can now only be constructed on the main thread, meaning you have to
36-
aquire a `MainThreadMarker` first.
34+
* **BREAKING**: Marked UI-related classes as `MainThreadOnly`, and UI-related
35+
protocols as `IsMainThreadOnly`.
36+
37+
This means that they can now only be constructed, retrieved and used on the
38+
main thread, meaning you usually have to aquire a `MainThreadMarker` first.
3739

3840
```rust
3941
// Before

crates/icrate/src/common.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ pub(crate) use std::os::raw::{
1414
pub(crate) use objc2::ffi::{NSInteger, NSIntegerMax, NSUInteger, NSUIntegerMax, IMP};
1515
#[cfg(feature = "objective-c")]
1616
pub(crate) use objc2::mutability::{
17-
Immutable, ImmutableWithMutableSubclass, InteriorMutable, IsIdCloneable, MainThreadOnly,
18-
Mutable, MutableWithImmutableSuperclass,
17+
Immutable, ImmutableWithMutableSubclass, InteriorMutable, IsIdCloneable, IsMainThreadOnly,
18+
MainThreadOnly, Mutable, MutableWithImmutableSuperclass,
1919
};
2020
#[cfg(feature = "objective-c")]
2121
pub(crate) use objc2::rc::{Allocated, DefaultId, Id};

0 commit comments

Comments
 (0)