Skip to content

Commit fd1aa3b

Browse files
committed
optimize function pointers to avoid heap allocation
1 parent 1d05f91 commit fd1aa3b

3 files changed

Lines changed: 138 additions & 64 deletions

File tree

‎docs/src/runtime/fn-ptr.md‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ impl<T> FnPtr<T> {
2121
`FnPtr` dereferences to the function, so a call through it is `(*fp)(args)`.
2222
Calling a null pointer panics with `ub:`.
2323

24-
`FnPtr` stores its function type-erased and identifies it by address through the
25-
`FnAddr` trait. Rust has no way to write an impl for every `fn` arity at once,
26-
so `FnAddr` is implemented by a macro for `fn` types of zero to sixteen
27-
parameters. A function with more parameters cannot be wrapped in an `FnPtr`, and
28-
taking its address fails to compile with a missing `FnAddr` bound.
24+
`FnPtr` stores the function inline, together with its address, which is how
25+
pointers are compared; the `FnAddr` trait provides the address. Creating,
26+
copying, and calling a function pointer does not allocate. Rust has no way to
27+
write an impl for every `fn` arity at once, so `FnAddr` is implemented by a
28+
macro for `fn` types of zero to sixteen parameters. A function with more
29+
parameters cannot be wrapped in an `FnPtr`, and taking its address fails to
30+
compile with a missing `FnAddr` bound.
2931

3032
```cpp
3133
typedef int (*int_fn)(int);
@@ -80,7 +82,10 @@ The code generator can build an adapter when the arguments and return type of
8082
the two function types have the same representation. Otherwise it passes `None`,
8183
and calling through the cast pointer panics with `ub:`.
8284

83-
Equality compares the address of the function the pointer was created with.
85+
A cast to a different type is the only operation that allocates: the pointer
86+
then also keeps the function it was created with, type-erased, so that casting
87+
back to that type can restore it. Equality compares the address of the function
88+
the pointer was created with.
8489

8590
Casting a function pointer to `void *` is `to_any`, and `AnyPtr::cast_fn::<T>`
8691
recovers it. `reinterpret_cast` on an `AnyPtr` holding a function currently

‎libcc2rs/src/fn_ptr.rs‎

Lines changed: 125 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Distributed under the MIT license that can be found in the LICENSE file.
33

44
use std::any::{Any, TypeId};
5-
use std::marker::PhantomData;
65
use std::ops::Deref;
76
use std::rc::Rc;
87

@@ -32,96 +31,96 @@ macro_rules! impl_fn_addr {
3231
}
3332
impl_fn_addr!();
3433

35-
trait ErasedFn: Any {
36-
fn addr(&self) -> usize;
37-
}
38-
39-
impl<T: FnAddr + Any> ErasedFn for T {
40-
fn addr(&self) -> usize {
41-
self.fn_addr()
42-
}
43-
}
44-
4534
pub struct FnPtr<T> {
46-
original: Option<Rc<dyn ErasedFn>>,
47-
current_cast: Option<Rc<dyn ErasedFn>>,
48-
// FnPtr does not use T, hence wrap in PhantomData
49-
_marker: PhantomData<T>,
35+
// Address of the function the pointer was created from. 0 for null, which
36+
// is never the address of a function.
37+
addr: usize,
38+
// The function as callable through T. None if the pointer was cast to T
39+
// without an adapter, when calling it is UB.
40+
current: Option<T>,
41+
// The function the pointer was created from, kept type-erased once the
42+
// pointer is cast to a different type so that it can be cast back. While
43+
// the pointer keeps its type this is None (and `current` is the original),
44+
// so creating and copying function pointers does not allocate.
45+
original: Option<Rc<dyn Any>>,
5046
}
5147

5248
impl<T> FnPtr<T> {
5349
#[inline]
5450
pub fn null() -> Self {
5551
FnPtr {
52+
addr: 0,
53+
current: None,
5654
original: None,
57-
current_cast: None,
58-
_marker: PhantomData,
5955
}
6056
}
6157

6258
#[inline]
6359
pub fn is_null(&self) -> bool {
64-
self.original.is_none()
60+
self.addr == 0
6561
}
6662
}
6763

6864
impl<T: FnAddr + 'static> FnPtr<T> {
65+
#[inline]
6966
pub fn new(f: T) -> Self {
70-
let rc: Rc<dyn ErasedFn> = Rc::new(f);
7167
FnPtr {
72-
original: Some(rc.clone()),
73-
current_cast: Some(rc),
74-
_marker: PhantomData,
68+
addr: f.fn_addr(),
69+
current: Some(f),
70+
original: None,
7571
}
7672
}
7773
}
7874

79-
impl<T: 'static> FnPtr<T> {
80-
pub fn cast<U: FnAddr + 'static>(&self, adapter: Option<U>) -> FnPtr<U> {
81-
let original = self.original.as_ref().expect("ub: null fn pointer cast");
75+
impl<T: FnAddr + Copy + 'static> FnPtr<T> {
76+
pub fn cast<U: FnAddr + Copy + 'static>(&self, adapter: Option<U>) -> FnPtr<U> {
77+
assert!(!self.is_null(), "ub: null fn pointer cast");
78+
79+
// The current function, if it already has type U.
80+
let current_as_u = self.current.as_ref().and_then(|current| {
81+
let current: &dyn Any = current;
82+
current.downcast_ref::<U>().copied()
83+
});
84+
// The function this pointer was created from, if it has type U. While
85+
// `original` is unset, that is the current function.
86+
let original_as_u = match &self.original {
87+
Some(original) => original.downcast_ref::<U>().copied(),
88+
None => current_as_u,
89+
};
8290

83-
let current_cast = if self
84-
.current_cast
85-
.as_ref()
86-
.is_some_and(|rc| Any::type_id(&**rc) == TypeId::of::<U>())
87-
{
88-
self.current_cast.clone()
89-
} else if Any::type_id(&**original) == TypeId::of::<U>() {
90-
Some(original.clone())
91-
} else {
92-
adapter.map(|a| Rc::new(a) as Rc<dyn ErasedFn>)
91+
let original = match &self.original {
92+
Some(original) => Some(original.clone()),
93+
// Casting to the same type keeps the pointer as is.
94+
None if TypeId::of::<T>() == TypeId::of::<U>() => None,
95+
None => self.current.map(|current| Rc::new(current) as Rc<dyn Any>),
9396
};
9497

9598
FnPtr {
96-
original: Some(original.clone()),
97-
current_cast,
98-
_marker: PhantomData,
99+
addr: self.addr,
100+
current: current_as_u.or(original_as_u).or(adapter),
101+
original,
99102
}
100103
}
101104
}
102105

103106
impl<T: 'static> Deref for FnPtr<T> {
104107
type Target = T;
105108
fn deref(&self) -> &T {
106-
if self.original.is_none() {
109+
if self.is_null() {
107110
panic!("ub: null fn pointer call");
108111
}
109-
let rc = self
110-
.current_cast
112+
self.current
111113
.as_ref()
112-
.expect("ub: calling through incompatible fn pointer type");
113-
let any: &dyn Any = &**rc;
114-
any.downcast_ref::<T>()
115-
.expect("ub: fn pointer type mismatch")
114+
.expect("ub: calling through incompatible fn pointer type")
116115
}
117116
}
118117

119-
impl<T> Clone for FnPtr<T> {
118+
impl<T: Copy> Clone for FnPtr<T> {
120119
fn clone(&self) -> Self {
121120
FnPtr {
121+
addr: self.addr,
122+
current: self.current,
122123
original: self.original.clone(),
123-
current_cast: self.current_cast.clone(),
124-
_marker: PhantomData,
125124
}
126125
}
127126
}
@@ -134,19 +133,15 @@ impl<T> Default for FnPtr<T> {
134133

135134
impl<T> PartialEq for FnPtr<T> {
136135
fn eq(&self, other: &Self) -> bool {
137-
match (&self.original, &other.original) {
138-
(None, None) => true,
139-
(Some(a), Some(b)) => a.addr() == b.addr(),
140-
_ => false,
141-
}
136+
self.addr == other.addr
142137
}
143138
}
144139

145140
impl<T> Eq for FnPtr<T> {}
146141

147142
impl<T: 'static> ByteRepr for FnPtr<T> {}
148143

149-
impl<T: 'static> ErasedPtr for FnPtr<T> {
144+
impl<T: Copy + 'static> ErasedPtr for FnPtr<T> {
150145
fn as_bytes(&self) -> Ptr<u8> {
151146
panic!("byte view not supported on fn pointer");
152147
}
@@ -161,7 +156,7 @@ impl<T: 'static> ErasedPtr for FnPtr<T> {
161156
}
162157
}
163158

164-
impl<T: 'static> FnPtr<T> {
159+
impl<T: Copy + 'static> FnPtr<T> {
165160
pub fn to_any(&self) -> AnyPtr {
166161
AnyPtr {
167162
ptr: Rc::new(self.clone()),
@@ -170,7 +165,81 @@ impl<T: 'static> FnPtr<T> {
170165
}
171166

172167
impl AnyPtr {
173-
pub fn cast_fn<T: 'static>(&self) -> Option<FnPtr<T>> {
168+
pub fn cast_fn<T: Copy + 'static>(&self) -> Option<FnPtr<T>> {
174169
self.ptr.as_any().downcast_ref::<FnPtr<T>>().cloned()
175170
}
176171
}
172+
173+
#[cfg(test)]
174+
mod tests {
175+
use super::*;
176+
177+
fn add_one(x: i32) -> i32 {
178+
x + 1
179+
}
180+
181+
fn twice(x: i32) -> i32 {
182+
x * 2
183+
}
184+
185+
#[test]
186+
fn call_null_and_equality() {
187+
let f = FnPtr::<fn(i32) -> i32>::new(add_one);
188+
assert!(!f.is_null());
189+
assert_eq!((*f)(1), 2);
190+
assert!(f == f.clone());
191+
assert!(f != FnPtr::new(twice as fn(i32) -> i32));
192+
assert!(FnPtr::<fn(i32) -> i32>::null().is_null());
193+
assert!(FnPtr::<fn(i32) -> i32>::null() == FnPtr::default());
194+
assert!(f != FnPtr::null());
195+
}
196+
197+
#[test]
198+
#[should_panic(expected = "ub: null fn pointer call")]
199+
fn call_null_panics() {
200+
let f = FnPtr::<fn(i32) -> i32>::null();
201+
(*f)(1);
202+
}
203+
204+
#[test]
205+
#[should_panic(expected = "ub: null fn pointer cast")]
206+
fn cast_null_panics() {
207+
FnPtr::<fn(i32) -> i32>::null().cast::<fn(u32) -> u32>(None);
208+
}
209+
210+
#[test]
211+
fn cast_with_adapter_and_back() {
212+
let f = FnPtr::<fn(i32) -> i32>::new(add_one);
213+
let g =
214+
f.cast::<fn(u32) -> u32>(Some((|x: u32| add_one(x as i32) as u32) as fn(u32) -> u32));
215+
assert_eq!((*g)(4), 5);
216+
// Equality is by the original function.
217+
assert!(g == f.cast::<fn(u32) -> u32>(None));
218+
// Casting back recovers the original function, without an adapter.
219+
let h = g.cast::<fn(i32) -> i32>(None);
220+
assert!(h == f);
221+
assert_eq!((*h)(4), 5);
222+
// Casting to the same type keeps the pointer.
223+
let same = f.cast::<fn(i32) -> i32>(None);
224+
assert_eq!((*same)(9), 10);
225+
}
226+
227+
#[test]
228+
#[should_panic(expected = "ub: calling through incompatible fn pointer type")]
229+
fn cast_without_adapter_cannot_be_called() {
230+
let f = FnPtr::<fn(i32) -> i32>::new(add_one);
231+
let g = f.cast::<fn(u32) -> u32>(None);
232+
(*g)(1);
233+
}
234+
235+
#[test]
236+
fn any_ptr_roundtrip() {
237+
let f = FnPtr::<fn(i32) -> i32>::new(twice);
238+
let any = f.to_any();
239+
assert!(!any.is_null());
240+
let back = any.cast_fn::<fn(i32) -> i32>().unwrap();
241+
assert_eq!((*back)(21), 42);
242+
assert!(back == f);
243+
assert!(any.cast_fn::<fn(u8)>().is_none());
244+
}
245+
}

‎libcc2rs/src/va_args.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,13 @@ impl VaArgGet for AnyPtr {
137137
}
138138
}
139139

140-
impl<T: 'static> From<crate::FnPtr<T>> for VaArg {
140+
impl<T: Copy + 'static> From<crate::FnPtr<T>> for VaArg {
141141
fn from(v: crate::FnPtr<T>) -> Self {
142142
VaArg::Ptr(v.to_any())
143143
}
144144
}
145145

146-
impl<T: 'static> VaArgGet for crate::FnPtr<T> {
146+
impl<T: Copy + 'static> VaArgGet for crate::FnPtr<T> {
147147
fn get(v: &VaArg) -> Self {
148148
match v {
149149
VaArg::Ptr(any) => any.cast_fn::<T>().expect("VaArgGet: FnPtr type mismatch"),

0 commit comments

Comments
 (0)