Skip to content

Commit 2192cdf

Browse files
committed
feat: add guideline in own file
1 parent 29d81b1 commit 2192cdf

File tree

2 files changed

+235
-0
lines changed

2 files changed

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

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)