Skip to content

Commit 443ac7a

Browse files
committed
feat: add guideline in own file
1 parent 29d81b1 commit 443ac7a

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
.. SPDX-License-Identifier: MIT OR Apache-2.0
2+
SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors
3+
4+
.. default-domain:: coding-guidelines
5+
6+
.. guideline:: Do not depend on function pointer identity
7+
:id: gui_QbvIknd9qNF6
8+
:category: required
9+
:status: draft
10+
:release: unclear-latest
11+
:fls: fls_1kg1mknf4yx7
12+
:decidability: decidable
13+
:scope: system
14+
:tags: surprising-behavior
15+
16+
Do not rely on the equality or stable identity of function pointers.
17+
18+
**Exception**
19+
20+
``#[no_mangle]`` functions are guaranteed to have a single instance.
21+
22+
.. rationale::
23+
:id: rat_kYiIiW8R2qD3
24+
:status: draft
25+
26+
Functions may be instantiated multiple times.
27+
They may, for example, be instantiated every time they are referenced.
28+
Only ``#[no_mangle]`` functions are guaranteed to be instantiated a single time,
29+
but can cause undefined behavior if they share a symbol with other identifiers.
30+
31+
Avoid assumptions about low-level metadata (such as symbol addresses) unless explicitly guaranteed by the Ferrocene Language Specification (FLS).
32+
Function address identity is not guaranteed and must not be treated as stable.
33+
Rust’s ``fn`` type is a zero-sized function item promoted to a function pointer, whose address is determined by the compiler backend.
34+
When a function resides in a different crate or codegen-unit partitioning is enabled,
35+
the compiler may generate multiple distinct code instances for the same function or alter the address at which it is emitted.
36+
37+
Consequently, the following operations are unreliable for functions which are not ``#[no_mangle]``:
38+
39+
- Comparing function pointers for equality (``fn1 == fn2``) 1Code has comments. Press enter to view.
40+
- Assuming a unique function address
41+
- Using function pointers as identity keys (e.g., in maps, registries, matchers) 1Code has comments. Press enter to view.
42+
- Matching behavior based on function address unless you instruct the linker to put a (#[no_mangle]) function at a specific address
43+
44+
This rule applies even when the functions are semantically identical, exported as ``pub``, or defined once in source form.
45+
46+
Compiler optimizations may cause function pointers to lose stable identity, for example:
47+
48+
- Cross-crate inlining can produce multiple code instantiations
49+
- Codegen-unit separation can cause function emission in multiple codegen units
50+
- Identical function implementations may be automatically merged as an optimization.
51+
Functions that are equivalent based only on specific hardware semantics may be merged in the machine-specific backend. Merging may also be performed as link-time optimization.
52+
53+
This behavior has resulted in real-world issues,
54+
such as the bug reported in `rust-lang/rust#117047 <https://github.com/rust-lang/rust/issues/117047>`_,
55+
where function pointer comparisons unexpectedly failed because the function in question was instantiated multiple times.
56+
57+
Violating this rule may cause:
58+
59+
- Silent logic failures: callbacks not matching, dispatch tables misbehaving.
60+
- Inappropriate branching: identity-based dispatch selecting wrong handler.
61+
- Security issues: adversary-controlled conditions bypassing function-based authorization/dispatch logic.
62+
- Nondeterministic behavior: correctness depending on build flags or incremental state.
63+
- Test-only correctness: function pointer equality passing in debug builds but failing in release/link-time optimization builds.
64+
65+
In short, dependence on function address stability introduces non-portable, build-profile-dependent behavior,
66+
which is incompatible with high-integrity Rust.
67+
68+
69+
.. rationale::
70+
:id: rat_xcVE5Hfnbb2u
71+
:status: draft
72+
73+
Compiler optimizations may cause function pointers to lose stable identity, for example:
74+
75+
- Cross-crate inlining can produce multiple code instantiations
76+
- Codegen-unit separation can cause function emission in multiple codegen units
77+
- Identical function implementations may be automatically merged as an optimization.
78+
Functions that are equivalent based only on specific hardware semantics may be merged in the machine-specific backend.
79+
Merging may also be performed as link-time optimization.
80+
81+
This behavior has resulted in real-world issues,
82+
such as the bug reported in `rust-lang/rust#117047 <https://github.com/rust-lang/rust/issues/117047>`_,
83+
where function pointer comparisons unexpectedly failed because the function in question was instantiated multiple times.
84+
85+
Violating this rule may cause:
86+
87+
- Silent logic failures: callbacks not matching, dispatch tables misbehaving.
88+
- Inappropriate branching: identity-based dispatch selecting wrong handler.
89+
- Security issues: adversary-controlled conditions bypassing function-based authorization/dispatch logic.
90+
- Nondeterministic behavior: correctness depending on build flags or incremental state.
91+
- Test-only correctness: function pointer equality passing in debug builds but failing in release/link-time optimization builds.
92+
93+
In short, dependence on function address stability introduces non-portable, build-profile-dependent behavior,
94+
which is incompatible with high-integrity Rust.
95+
96+
.. non_compliant_example::
97+
:id: non_compl_ex_MkAkFxjRTijx
98+
:status: draft
99+
100+
Due to cross-crate inlining or codegen-unit partitioning,
101+
the address of ``handler_a`` in crate ``B`` may differ from its address in crate A,
102+
causing comparisons to fail as shown in this noncompliant code example:
103+
104+
.. rust-example::
105+
106+
// crate A
107+
pub fn handler_a() {}
108+
pub fn handler_b() {}
109+
110+
// crate B
111+
use crate_a::{handler_a, handler_b};
112+
113+
fn dispatch(f: fn()) {
114+
if f == handler_a {
115+
println!("Handled by A");
116+
} else if f == handler_b {
117+
println!("Handled by B");
118+
}
119+
}
120+
121+
dispatch(handler_a);
122+
123+
// Error: This may fail unpredictably if handler_a is inlined or duplicated.
124+
125+
.. compliant_example::
126+
:id: compl_ex_oiqSSclTXmIi
127+
:status: draft
128+
129+
Replace function pointer comparison with an explicit enum as shown in this compliant example:
130+
131+
.. rust-example::
132+
133+
// crate A
134+
pub enum HandlerId { A, B }
135+
136+
pub fn handler(id: HandlerId) {
137+
match id {
138+
HandlerId::A => handler_a(),
139+
HandlerId::B => handler_b(),
140+
}
141+
}
142+
143+
// crate B
144+
use crate_a::{handler, HandlerId};
145+
146+
fn dispatch(id: HandlerId) {
147+
handler(id);
148+
}
149+
150+
dispatch(HandlerId::A); // OK: semantically stable identity
151+
152+
.. non_compliant_example::
153+
:id: non_compl_ex_MkAkFxjRTijy
154+
:status: draft
155+
156+
A function pointer used as a key is not guaranteed to have stable identity, as shown in this noncompliant example:
157+
158+
.. rust-example::
159+
160+
// crate A
161+
pub fn op_mul(x: i32) -> i32 { x * 2 }
162+
163+
// crate B
164+
use crate_a::op_mul;
165+
use std::collections::HashMap;
166+
167+
let mut registry: HashMap<fn(i32) -> i32, &'static str> = HashMap::new();
168+
registry.insert(op_mul, "double");
169+
170+
let f = op_mul;
171+
172+
// Error: Lookup may fail if `op_mul` has multiple emitted instances.
173+
assert_eq!(registry.get(&f), Some(&"double"));
174+
175+
.. compliant_example::
176+
:id: compl_ex_oiqSSclTXmIj
177+
:status: draft
178+
179+
This compliant example uses a stable identity wrappers as identity keys.
180+
The ``id`` is a stable, programmer-defined identity, immune to compiler optimizations.
181+
The function pointer is preserved for behavior (``func``) but never used as the identity key.
182+
183+
.. rust-example::
184+
185+
// crate A
186+
187+
pub fn op_mul(x: i32) -> i32 { x * 2 }
188+
pub fn op_add(x: i32) -> i32 { x + 2 }
189+
190+
// Stable identity wrapper for an operation.
191+
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
192+
pub struct Operation {
193+
pub id: u32,
194+
pub func: fn(i32) -> i32,
195+
}
196+
197+
// Export stable descriptors.
198+
pub const OP_MUL: Operation = Operation { id: 1, func: op_mul };
199+
pub const OP_ADD: Operation = Operation { id: 2, func: op_add };
200+
201+
// crate B
202+
203+
use crate_a::{Operation, OP_MUL, OP_ADD};
204+
use std::collections::HashMap;
205+
206+
fn main() {
207+
let mut registry: HashMap<u32, &'static str> = HashMap::new();
208+
209+
// Insert using stable identity key (ID), not function pointer.
210+
registry.insert(OP_MUL.id, "double");
211+
registry.insert(OP_ADD.id, "increment");
212+
213+
// Later: lookup using ID
214+
let op = OP_MUL;
215+
216+
// lookup works reliably regardless of inlining, LTO, CGUs, cross-crate instantiation, etc.
217+
assert_eq!(registry.get(&op.id), Some(&"double"));
218+
219+
println!("OP_MUL maps to: {}", registry[&op.id]);
220+
}
221+
222+
.. non_compliant_example::
223+
:id: non_compl_ex_MkAkFxjRTijz
224+
:status: draft
225+
226+
This noncompliant example relies on function pointer identity for deduplication:
227+
228+
.. rust-example::
229+
230+
// crate B
231+
let mut handlers: Vec<fn()> = Vec::new();
232+
233+
fn register(h: fn()) {
234+
if !handlers.contains(&h) {
235+
handlers.push(h);
236+
}
237+
}
238+
239+
register(handler); // Error: may be inserted twice under some builds
240+
241+
.. compliant_example::
242+
:id: compl_ex_oiqSSclTXmIk
243+
:status: draft
244+
245+
This compliant example keeps identity-sensitive logic inside a single crate:
246+
247+
.. rust-example::
248+
249+
// crate A (single crate boundary)
250+
#[inline(never)]
251+
pub fn important_handler() {}
252+
253+
pub fn is_important(f: fn()) -> bool {
254+
// Safe because identity and comparison are confined to one crate,
255+
// and inlining is prohibited.
256+
f == important_handler
257+
}

src/coding-guidelines/types-and-traits/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Types and Traits
77
================
88

99
.. include:: gui_xztNdXA2oFNC.rst.inc
10+
.. include:: gui_QbvIknd9qNF6.rst.inc

0 commit comments

Comments
 (0)