doc: define unambiguous type mappings across C and Rust

Document other nuances when crossing the FFI boundary. Other language mappings may be added in the future. Signed-off-by: Ezekiel Newren <ezekielnewren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ezekiel Newren committed Nov 18, 2025 at 22:34 UTC 6971934d9bb4b8176b48658482862169a4582913
3 files changed +226
Documentation/Makefile
+1
@@ -140,6 +140,7 @@ TECH_DOCS += technical/shallow
140 TECH_DOCS += technical/sparse-checkout
141 TECH_DOCS += technical/sparse-index
142 TECH_DOCS += technical/trivial-merge
143 +TECH_DOCS += technical/unambiguous-types
144 TECH_DOCS += technical/unit-tests
145 SP_ARTICLES += $(TECH_DOCS)
146 SP_ARTICLES += technical/api-index
Documentation/technical/meson.build
+1
@@ -31,6 +31,7 @@ articles = [
31 'sparse-checkout.adoc',
32 'sparse-index.adoc',
33 'trivial-merge.adoc',
34 + 'unambiguous-types.adoc',
35 'unit-tests.adoc',
36 ]
37
Documentation/technical/unambiguous-types.adoc new
+224
@@ -0,0 +1,224 @@
1 += Unambiguous types
2 +
3 +Most of these mappings are obvious, but there are some nuances and gotchas with
4 +Rust FFI (Foreign Function Interface).
5 +
6 +This document defines clear, one-to-one mappings between primitive types in C,
7 +Rust (and possible other languages in the future). Its purpose is to eliminate
8 +ambiguity in type widths, signedness, and binary representation across
9 +platforms and languages.
10 +
11 +For Git, the only header required to use these unambiguous types in C is
12 +`git-compat-util.h`.
13 +
14 +== Boolean types
15 +[cols="1,1", options="header"]
16 +|===
17 +| C Type | Rust Type
18 +| bool^1^ | bool
19 +|===
20 +
21 +== Integer types
22 +
23 +In C, `<stdint.h>` (or an equivalent) must be included.
24 +
25 +[cols="1,1", options="header"]
26 +|===
27 +| C Type | Rust Type
28 +| uint8_t | u8
29 +| uint16_t | u16
30 +| uint32_t | u32
31 +| uint64_t | u64
32 +
33 +| int8_t | i8
34 +| int16_t | i16
35 +| int32_t | i32
36 +| int64_t | i64
37 +|===
38 +
39 +== Floating-point types
40 +
41 +Rust requires IEEE-754 semantics.
42 +In C, that is typically true, but not guaranteed by the standard.
43 +
44 +[cols="1,1", options="header"]
45 +|===
46 +| C Type | Rust Type
47 +| float^2^ | f32
48 +| double^2^ | f64
49 +|===
50 +
51 +== Size types
52 +
53 +These types represent pointer-sized integers and are typically defined in
54 +`<stddef.h>` or an equivalent header.
55 +
56 +Size types should be used any time pointer arithmetic is performed e.g.
57 +indexing an array, describing the number of elements in memory, etc...
58 +
59 +[cols="1,1", options="header"]
60 +|===
61 +| C Type | Rust Type
62 +| size_t^3^ | usize
63 +| ptrdiff_t^3^ | isize
64 +|===
65 +
66 +== Character types
67 +
68 +This is where C and Rust don't have a clean one-to-one mapping.
69 +
70 +A C `char` and a Rust `u8` share the same bit width, so any C struct containing
71 +a `char` will have the same size as the corresponding Rust struct using `u8`.
72 +In that sense, such structs are safe to pass over the FFI boundary, because
73 +their fields will be laid out identically. However, beyond bit width, C `char`
74 +has additional semantics and platform-dependent behavior that can cause
75 +problems, as discussed below.
76 +
77 +The C language leaves the signedness of `char` implementation defined. Because
78 +our developer build enables -Wsign-compare, comparison of a value of `char`
79 +type with either signed or unsigned integers may trigger warnings from the
80 +compiler.
81 +
82 +Note: Rust's `char` type is an unsigned 32-bit integer that is used to describe
83 +Unicode code points.
84 +
85 +=== Notes
86 +^1^ This is only true if stdbool.h (or equivalent) is used. +
87 +^2^ C does not enforce IEEE-754 compatibility, but Rust expects it. If the
88 +platform/arch for C does not follow IEEE-754 then this equivalence does not
89 +hold. Also, it's assumed that `float` is 32 bits and `double` is 64, but
90 +there may be a strange platform/arch where even this isn't true. +
91 +^3^ C also defines uintptr_t, ssize_t and intptr_t, but these types are
92 +discouraged for FFI purposes. For functions like `read()` and `write()` ssize_t
93 +should be cast to a different, and unambiguous, type before being passed over
94 +the FFI boundary. +
95 +
96 +== Problems with std::ffi::c_* types in Rust
97 +TL;DR: In practice, Rust's `c_*` types aren't guaranteed to match C types for
98 +all possible C compilers, platforms, or architectures, because Rust only
99 +ensures correctness of C types on officially supported targets. These
100 +definitions have changed over time to match more targets which means that the
101 +c_* definitions will differ based on which Rust version Git chooses to use.
102 +
103 +Current list of safe, Rust side, FFI types in Git: +
104 +
105 +* `c_void`
106 +* `CStr`
107 +* `CString`
108 +
109 +Even then, they should be used sparingly, and only where the semantics match
110 +exactly.
111 +
112 +The std::os::raw::c_* directly inherits the problems of core::ffi, which
113 +changes over time and seems to make a best guess at the correct definition for
114 +a given platform/target. This probably isn't a problem for all other platforms
115 +that Rust supports currently, but can anyone say that Rust got it right for all
116 +C compilers of all platforms/targets?
117 +
118 +To give an example: c_long is defined in
119 +footnote:[https://doc.rust-lang.org/1.63.0/src/core/ffi/mod.rs.html#175-189[c_long in 1.63.0]]
120 +footnote:[https://doc.rust-lang.org/1.89.0/src/core/ffi/primitives.rs.html#135-151[c_long in 1.89.0]]
121 +
122 +=== Rust version 1.63.0
123 +
124 +```
125 +mod c_long_definition {
126 + cfg_if! {
127 + if #[cfg(all(target_pointer_width = "64", not(windows)))] {
128 + pub type c_long = i64;
129 + pub type NonZero_c_long = crate::num::NonZeroI64;
130 + pub type c_ulong = u64;
131 + pub type NonZero_c_ulong = crate::num::NonZeroU64;
132 + } else {
133 + // The minimal size of `long` in the C standard is 32 bits
134 + pub type c_long = i32;
135 + pub type NonZero_c_long = crate::num::NonZeroI32;
136 + pub type c_ulong = u32;
137 + pub type NonZero_c_ulong = crate::num::NonZeroU32;
138 + }
139 + }
140 +}
141 +```
142 +
143 +=== Rust version 1.89.0
144 +
145 +```
146 +mod c_long_definition {
147 + crate::cfg_select! {
148 + any(
149 + all(target_pointer_width = "64", not(windows)),
150 + // wasm32 Linux ABI uses 64-bit long
151 + all(target_arch = "wasm32", target_os = "linux")
152 + ) => {
153 + pub(super) type c_long = i64;
154 + pub(super) type c_ulong = u64;
155 + }
156 + _ => {
157 + // The minimal size of `long` in the C standard is 32 bits
158 + pub(super) type c_long = i32;
159 + pub(super) type c_ulong = u32;
160 + }
161 + }
162 +}
163 +```
164 +
165 +Even for the cases where C types are correctly mapped to Rust types via
166 +std::ffi::c_* there are still problems. Let's take c_char for example. On some
167 +platforms it's u8 on others it's i8.
168 +
169 +=== Subtraction underflow in debug mode
170 +
171 +The following code will panic in debug on platforms that define c_char as u8,
172 +but won't if it's an i8.
173 +
174 +```
175 +let mut x: std::ffi::c_char = 0;
176 +x -= 1;
177 +```
178 +
179 +=== Inconsistent shift behavior
180 +
181 +`x` will be 0xC0 for platforms that use i8, but will be 0x40 where it's u8.
182 +
183 +```
184 +let mut x: std::ffi::c_char = 0x80;
185 +x >>= 1;
186 +```
187 +
188 +=== Equality fails to compile on some platforms
189 +
190 +The following will not compile on platforms that define c_char as i8, but will
191 +if it's u8. You can cast x e.g. `assert_eq!(x as u8, b'a');`, but then you get
192 +a warning on platforms that use u8 and a clean compilation where i8 is used.
193 +
194 +```
195 +let mut x: std::ffi::c_char = 0x61;
196 +assert_eq!(x, b'a');
197 +```
198 +
199 +== Enum types
200 +Rust enum types should not be used as FFI types. Rust enum types are more like
201 +C union types than C enum's. For something like:
202 +
203 +```
204 +#[repr(C, u8)]
205 +enum Fruit {
206 + Apple,
207 + Banana,
208 + Cherry,
209 +}
210 +```
211 +
212 +It's easy enough to make sure the Rust enum matches what C would expect, but a
213 +more complex type like.
214 +
215 +```
216 +enum HashResult {
217 + SHA1([u8; 20]),
218 + SHA256([u8; 32]),
219 +}
220 +```
221 +
222 +The Rust compiler has to add a discriminant to the enum to distinguish between
223 +the variants. The width, location, and values for that discriminant is up to
224 +the Rust compiler and is not ABI stable.