Skip to content

Commit ace5af3

Browse files
committed
Use static encoding strings in define_class! methods
For optimization, and as a first step towards #604
1 parent 1033a13 commit ace5af3

File tree

22 files changed

+499
-389
lines changed

22 files changed

+499
-389
lines changed

crates/objc2/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1212
* Added support for `&CStr` in method argument and return types.
1313
* Implement `Encode` on `Result<(), T>` when `T: OptionEncode`.
1414
* Allow returning `Result<T, Retained<NSError>>` from `define_class!`.
15+
* Optimized adding `define_class!` methods to avoid a few string allocations.
1516

1617
## Changed
1718
* **BREAKING**: Changed syntax for `define_class!` ivars (instance variables).

crates/objc2/src/__macros/define_class/checks.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use core::panic::{RefUnwindSafe, UnwindSafe};
66
#[cfg(debug_assertions)]
77
use std::collections::HashSet;
88

9+
use crate::encode::{EncodeArguments, EncodeReturn};
910
use crate::runtime::{AnyClass, ClassBuilder, MethodImplementation, Sel};
1011
#[cfg(debug_assertions)]
1112
use crate::runtime::{AnyProtocol, MethodDescription};
@@ -215,21 +216,43 @@ impl<T: DefinedClass> ClassBuilderHelper<T> {
215216

216217
// Addition: This restricts to callee `T`
217218
#[inline]
218-
pub unsafe fn add_method<F>(&mut self, sel: Sel, func: F)
219+
pub unsafe fn add_method<F>(&mut self, sel: Sel, func: F, encoding: &CStr)
219220
where
220221
F: MethodImplementation<Callee = T>,
221222
{
223+
if cfg!(all(
224+
debug_assertions,
225+
not(feature = "disable-encoding-assertions")
226+
)) {
227+
self.builder
228+
.verify_method(sel, F::Arguments::ENCODINGS, &F::Return::ENCODING_RETURN);
229+
}
230+
222231
// SAFETY: Checked by caller
223-
unsafe { self.builder.add_method(sel, func) }
232+
unsafe { self.builder.add_method_inner(sel, func.__imp(), encoding) }
224233
}
225234

226235
#[inline]
227-
pub unsafe fn add_class_method<F>(&mut self, sel: Sel, func: F)
236+
pub unsafe fn add_class_method<F>(&mut self, sel: Sel, func: F, encoding: &CStr)
228237
where
229238
F: MethodImplementation<Callee = AnyClass>,
230239
{
240+
if cfg!(all(
241+
debug_assertions,
242+
not(feature = "disable-encoding-assertions")
243+
)) {
244+
self.builder.verify_class_method(
245+
sel,
246+
F::Arguments::ENCODINGS,
247+
&F::Return::ENCODING_RETURN,
248+
);
249+
}
250+
231251
// SAFETY: Checked by caller
232-
unsafe { self.builder.add_class_method(sel, func) }
252+
unsafe {
253+
self.builder
254+
.add_class_method_inner(sel, func.__imp(), encoding)
255+
}
233256
}
234257
}
235258

@@ -259,7 +282,7 @@ pub struct ClassProtocolMethodsBuilder<'a, T: ?Sized> {
259282
impl<T: DefinedClass> ClassProtocolMethodsBuilder<'_, T> {
260283
// Addition: This restricts to callee `T`
261284
#[inline]
262-
pub unsafe fn add_method<F>(&mut self, sel: Sel, func: F)
285+
pub unsafe fn add_method<F>(&mut self, sel: Sel, func: F, encoding: &CStr)
263286
where
264287
F: MethodImplementation<Callee = T>,
265288
{
@@ -279,7 +302,7 @@ impl<T: DefinedClass> ClassProtocolMethodsBuilder<'_, T> {
279302
}
280303

281304
// SAFETY: Checked by caller
282-
unsafe { self.builder.add_method(sel, func) };
305+
unsafe { self.builder.add_method(sel, func, encoding) };
283306

284307
#[cfg(debug_assertions)]
285308
if !self.registered_instance_methods.insert(sel) {
@@ -288,7 +311,7 @@ impl<T: DefinedClass> ClassProtocolMethodsBuilder<'_, T> {
288311
}
289312

290313
#[inline]
291-
pub unsafe fn add_class_method<F>(&mut self, sel: Sel, func: F)
314+
pub unsafe fn add_class_method<F>(&mut self, sel: Sel, func: F, encoding: &CStr)
292315
where
293316
F: MethodImplementation<Callee = AnyClass>,
294317
{
@@ -308,7 +331,7 @@ impl<T: DefinedClass> ClassProtocolMethodsBuilder<'_, T> {
308331
}
309332

310333
// SAFETY: Checked by caller
311-
unsafe { self.builder.add_class_method(sel, func) };
334+
unsafe { self.builder.add_class_method(sel, func, encoding) };
312335

313336
#[cfg(debug_assertions)]
314337
if !self.registered_class_methods.insert(sel) {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Helpers to construct encoding strings for methods at `const`-time.
2+
use objc2_encode::Encoding;
3+
4+
pub const fn method_encoding_str_len(ret: &Encoding, args: &[Encoding]) -> usize {
5+
let mut len = 0;
6+
len += ret.str_len();
7+
len += 1; // Encoding::Object
8+
len += 1; // Encoding::Sel
9+
let mut i = 0;
10+
while i < args.len() {
11+
len += args[i].str_len();
12+
i += 1;
13+
}
14+
len
15+
}
16+
17+
pub const fn method_encoding_str_array<const LEN: usize>(
18+
ret: &Encoding,
19+
args: &[Encoding],
20+
) -> [u8; LEN] {
21+
let mut res: [u8; LEN] = [0; LEN];
22+
let mut res_i = 0;
23+
24+
let mut i = 0;
25+
// We use LEN even though it creates an oversized array.
26+
let arr = ret.str_array::<LEN>();
27+
while i < ret.str_len() {
28+
res[res_i] = arr[i];
29+
i += 1;
30+
res_i += 1;
31+
}
32+
33+
// Encoding::Object
34+
res[res_i] = b'@';
35+
res_i += 1;
36+
37+
// Encoding::Sel
38+
res[res_i] = b':';
39+
res_i += 1;
40+
41+
let mut i = 0;
42+
while i < args.len() {
43+
let mut j = 0;
44+
// We use LEN even though it creates an oversized array.
45+
let arr = args[i].str_array::<LEN>();
46+
while j < args[i].str_len() {
47+
res[res_i] = arr[j];
48+
j += 1;
49+
res_i += 1;
50+
}
51+
i += 1;
52+
}
53+
res
54+
}
55+
56+
#[cfg(test)]
57+
mod tests {
58+
use super::*;
59+
use crate::encode::{Encode, EncodeReturn};
60+
use crate::runtime::{method_type_encoding, Bool, NSObject};
61+
62+
#[track_caller]
63+
fn check(ret: &Encoding, args: &[Encoding]) {
64+
// Compare against reference impl
65+
let expected = method_type_encoding(ret, args).into_string().unwrap();
66+
67+
let len = method_encoding_str_len(ret, args);
68+
assert_eq!(expected.len(), len, "incorrect length");
69+
assert!(len < 100);
70+
71+
let arr = method_encoding_str_array::<100>(ret, args);
72+
let (s, rest) = arr.split_at(len);
73+
let s = str::from_utf8(s).unwrap();
74+
assert_eq!(expected, s, "incorrect output: {arr:?}");
75+
76+
assert!(rest.iter().all(|x| *x == 0), "rest must be zero: {arr:?}");
77+
}
78+
79+
#[test]
80+
fn various() {
81+
check(&<()>::ENCODING_RETURN, &[]);
82+
check(
83+
&<*const NSObject>::ENCODING_RETURN,
84+
&[<&NSObject>::ENCODING],
85+
);
86+
check(
87+
&<&&&&&&i32>::ENCODING_RETURN,
88+
&[<&&&&&&NSObject>::ENCODING, <&&&&&&Bool>::ENCODING],
89+
);
90+
}
91+
}

crates/objc2/src/__macros/define_class/ivars.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ use core::ffi::CStr;
5252
use core::mem;
5353
use core::ptr::{self, NonNull};
5454

55+
use crate::__macros::{method_encoding_str_array, method_encoding_str_len};
5556
use crate::encode::{Encode, Encoding};
56-
use crate::runtime::{AnyClass, AnyObject, ClassBuilder, MessageReceiver, Sel};
57+
use crate::runtime::{
58+
AnyClass, AnyObject, ClassBuilder, MessageReceiver, MethodImplementation, Sel,
59+
};
5760
use crate::{sel, ClassType, DefinedClass};
5861

5962
/// A type representing the drop flags that may be set for a type.
@@ -144,9 +147,15 @@ where
144147
// Add dealloc if the class or the ivars need dropping.
145148
if mem::needs_drop::<T>() || mem::needs_drop::<T::Ivars>() {
146149
let func: unsafe extern "C-unwind" fn(_, _) = dealloc::<T>;
147-
// SAFETY: The function signature is correct, and method contract is
148-
// upheld inside `dealloc`.
149-
unsafe { builder.add_method(sel!(dealloc), func) };
150+
151+
const ENCODING_LEN: usize = method_encoding_str_len(&Encoding::Void, &[]);
152+
const ENCODING_ARRAY: [u8; ENCODING_LEN + 1] =
153+
method_encoding_str_array::<{ ENCODING_LEN + 1 }>(&Encoding::Void, &[]);
154+
const ENCODING: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(&ENCODING_ARRAY) };
155+
156+
// SAFETY: The function signature is correct, the encoding is correct,
157+
// and method contract is upheld inside `dealloc`.
158+
unsafe { builder.add_method_inner(sel!(dealloc), func.__imp(), ENCODING) };
150159
} else {
151160
// Users should not rely on this omission, it is only an optimization.
152161
}

crates/objc2/src/__macros/define_class/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
mod checks;
2+
mod encoding;
23
mod ivars;
34
mod output_impls;
45
mod register_impls;
56
mod thunk;
67

78
pub use self::checks::*;
9+
pub use self::encoding::*;
810
pub use self::ivars::*;
911
pub use self::thunk::*;
1012

crates/objc2/src/__macros/define_class/register_impls.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,12 +348,28 @@ macro_rules! __declare_class_register_thunk {
348348
// TODO: `<Self $(as $protocol)?>` once we emit on protocols.
349349
}
350350

351+
const __ENCODING_LEN: $crate::__macros::usize = $crate::__macros::method_encoding_str_len(
352+
&<$for as $crate::__macros::ConvertDefinedFn<'_, __FnMarker, __RetainSemantics, __Kind<'_>>>::RETURN_ENCODING,
353+
<$for as $crate::__macros::ConvertDefinedFn<'_, __FnMarker, __RetainSemantics, __Kind<'_>>>::ARGUMENT_ENCODINGS,
354+
);
355+
356+
// SAFETY: The array is guaranteed to be contain no interior NUL
357+
// bytes (`verify_name` in `objc2-encode`), and a NUL byte at the end
358+
// because we specify `__ENCODING_LEN + 1`.
359+
const __ENCODING: &$crate::__macros::CStr = unsafe {
360+
$crate::__macros::CStr::from_bytes_with_nul_unchecked(&$crate::__macros::method_encoding_str_array::<{__ENCODING_LEN + 1}>(
361+
&<$for as $crate::__macros::ConvertDefinedFn<'_, __FnMarker, __RetainSemantics, __Kind<'_>>>::RETURN_ENCODING,
362+
<$for as $crate::__macros::ConvertDefinedFn<'_, __FnMarker, __RetainSemantics, __Kind<'_>>>::ARGUMENT_ENCODINGS,
363+
))
364+
};
365+
351366
unsafe {
352367
$crate::__declare_class_call_builder_method! {
353368
($builder)
354369
($crate::sel!($($sel)*))
355370
(<Self as $crate::__macros::ConvertDefinedFn<'_, __FnMarker, __RetainSemantics, __Kind<'_>>>::THUNK)
356371
($($receiver_ty)?)
372+
(__ENCODING)
357373
}
358374
};
359375
);
@@ -385,8 +401,9 @@ macro_rules! __declare_class_call_builder_method {
385401
($sel:expr)
386402
($fnptr:expr)
387403
()
404+
($encoding:expr)
388405
} => {
389-
$builder.add_class_method($sel, $fnptr)
406+
$builder.add_class_method($sel, $fnptr, $encoding)
390407
};
391408

392409
// Instance method
@@ -395,8 +412,9 @@ macro_rules! __declare_class_call_builder_method {
395412
($sel:expr)
396413
($fnptr:expr)
397414
($_receiver_ty:ty)
415+
($encoding:expr)
398416
} => {
399-
$builder.add_method($sel, $fnptr)
417+
$builder.add_method($sel, $fnptr, $encoding)
400418
};
401419
}
402420

crates/objc2/src/__macros/define_class/thunk.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
//! few arguments compared to the selector.
2121
use core::marker::PhantomData;
2222

23+
use objc2_encode::Encoding;
24+
2325
use super::super::{ConvertArgument, ConvertError, KindDefined, RetainSemantics};
26+
use crate::encode::{EncodeArguments, EncodeReturn};
2427
use crate::rc::Retained;
2528
use crate::runtime::{MethodImplementation, Sel};
2629
use crate::ClassType;
@@ -49,6 +52,22 @@ pub trait ConvertDefinedFn<'f, FnMarker, MethodFamily, Kind>: ClassType {
4952
/// The generated thunk.
5053
const THUNK: <Self::Func as FnToExternFn<FnMarker, MethodFamily, Kind, Self>>::ExternFn =
5154
{ Self::Func::THUNK };
55+
56+
/// Helper for getting the return encoding of the extern function.
57+
const RETURN_ENCODING: Encoding = <<<Self::Func as FnToExternFn<
58+
FnMarker,
59+
MethodFamily,
60+
Kind,
61+
Self,
62+
>>::ExternFn as MethodImplementation>::Return as EncodeReturn>::ENCODING_RETURN;
63+
64+
/// Helper for getting the argument encodings of the extern function.
65+
const ARGUMENT_ENCODINGS: &'static [Encoding] = <<<Self::Func as FnToExternFn<
66+
FnMarker,
67+
MethodFamily,
68+
Kind,
69+
Self,
70+
>>::ExternFn as MethodImplementation>::Arguments as EncodeArguments>::ENCODINGS;
5271
}
5372

5473
/// Function pointers that can be converted to `extern "C-unwind"` function

crates/objc2/src/__macros/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub use core::marker::{PhantomData, Sized};
4747
pub use core::mem::{size_of, transmute, ManuallyDrop, MaybeUninit};
4848
pub use core::ops::Deref;
4949
pub use core::option::Option::{self, None, Some};
50-
pub use core::primitive::{bool, isize, str, u8};
50+
pub use core::primitive::{bool, isize, str, u8, usize};
5151
pub use core::{compile_error, concat, env, module_path, panic, stringify};
5252
pub use std::sync::Once;
5353

@@ -60,8 +60,9 @@ pub use self::convert::{
6060
ConvertArgument, ConvertArguments, ConvertError, ConvertReturn, TupleExtender,
6161
};
6262
pub use self::define_class::{
63-
class_c_name, define_class, ClassBuilderHelper, ClassFnKind, ClassFnResultKind,
64-
ClassProtocolMethodsBuilder, ConvertDefinedFn, DefinedIvarsHelper, LifetimeAssign, MethodKind,
63+
class_c_name, define_class, method_encoding_str_array, method_encoding_str_len,
64+
ClassBuilderHelper, ClassFnKind, ClassFnResultKind, ClassProtocolMethodsBuilder,
65+
ConvertDefinedFn, DefinedIvarsHelper, FnToExternFn, LifetimeAssign, MethodKind,
6566
MethodResultKind, ThreadKindAutoTraits,
6667
};
6768
pub use self::extern_class::{DoesNotImplDrop, MainThreadOnlyDoesNotImplSendSync, ValidThreadKind};

0 commit comments

Comments
 (0)