esp_idf_sys/stdio.rs
1//! Restoring the POSIX binding between the C standard streams and file
2//! descriptors 0, 1 and 2.
3//!
4//! ESP-IDF attaches `stdin`/`stdout`/`stderr` to the `/dev/console` VFS device.
5//! Opening `/dev/console` internally opens its backing devices (the primary
6//! console - usually UART - and, on chips which have one, the USB-Serial-JTAG
7//! secondary console) *first*, and VFS descriptors are handed out
8//! lowest-free-first. As a result, the standard streams end up on descriptors
9//! 2/3/4 (or similar), while descriptors 0/1/2 point to the raw backing devices.
10//!
11//! Anything which assumes the POSIX descriptor binding - most prominently the
12//! Rust Standard Library, which reads `stdin` from descriptor 0 and writes
13//! `println!`/`eprintln!` output to descriptors 1/2 - either loses its output,
14//! or panics with "failed printing to stdout: Success (os error 0)", as
15//! descriptor 1 is usually the raw USB-Serial-JTAG secondary console device,
16//! whose `write` fails without setting `errno` when no USB host is attached.
17//!
18//! Note that all of the above only applies when the I/O support of the VFS
19//! component is compiled in (`CONFIG_VFS_SUPPORT_IO`). Without it, ESP-IDF
20//! initializes the standard streams with the stock `__sinit` of the C library,
21//! which does bind them to descriptors 0, 1 and 2 natively.
22//!
23//! How the descriptors are re-arranged depends on the C library, because the
24//! two libraries ESP-IDF supports own their standard streams very differently:
25//!
26//! - With newlib, the streams are `fopen`-ed by ESP-IDF, so they can be closed
27//! and re-created, which is what the restoration does: it re-creates them at
28//! a point where descriptors 0, 1 and 2 are the lowest free ones.
29//! - With picolibc (the default from ESP-IDF v6.0 on), the streams are
30//! statically allocated `FILE`s owned by ESP-IDF and set up without picolibc's
31//! "allocated by stdio" flag. They must not be `fclose`-ed: `fclose` would
32//! neither close their descriptors nor free them, but it *would* destroy their
33//! locks, leaving each stream with a dangling handle to a deleted FreeRTOS
34//! mutex - the next stdio call on such a stream spins in `taskENTER_CRITICAL`
35//! on freed memory until the interrupt watchdog fires. And re-running
36//! ESP-IDF's stdio initialization would not re-create them either; it only
37//! re-opens the console and stamps the descriptors into the static streams -
38//! asserting, on top of that, that `stdin` does *not* land on descriptor 0.
39//! So for picolibc the standard streams are left alone (only their console
40//! handles are re-opened) and descriptors 0, 1 and 2 are claimed by console
41//! handles of our own.
42
43/// Restores the POSIX binding between the C standard streams and file
44/// descriptors 0, 1 and 2, so that reads from descriptor 0 and writes to
45/// descriptors 1 and 2 go to `/dev/console`.
46///
47/// Returns `true` if the binding is in place when the function returns
48/// (either because it was already in place, or because it was successfully
49/// restored), and `false` otherwise.
50///
51/// The function is idempotent. It is called automatically from the `app_main`
52/// glue of the `binstart`/`libstart` features, so calling it explicitly is
53/// only necessary with a custom `app_main`.
54///
55/// On ESP-IDF versions older than v5.3 the restoration is not attempted (the
56/// function only reports whether the binding happens to be in place): the
57/// console of those versions does not refcount its open/close calls, which
58/// the re-arrangement of the descriptors relies on. For the older versions,
59/// the `CONFIG_ESP_CONSOLE_SECONDARY_NONE=y` sdkconfig setting is a build-time
60/// alternative, as it results in descriptors 1 and 2 landing on the console.
61///
62/// NOTE: the standard streams are detached from the console and re-attached to
63/// it while the function runs, so it must be called *before* any other thread
64/// might be using them - which is why the `app_main` glue calls it first thing.
65#[allow(clippy::needless_bool)]
66pub fn restore_posix_stdio_fds() -> bool {
67 unsafe {
68 // Nothing to do if the streams are already on their POSIX descriptors:
69 // - a second call (newlib),
70 // - an ESP-IDF which binds them correctly,
71 // - VFS I/O support not compiled in, in which case ESP-IDF initializes
72 // the streams with the stock `__sinit` of the C library, which does
73 // bind them to descriptors 0, 1 and 2
74 if imp::bound() {
75 return true;
76 }
77
78 #[cfg(all(
79 esp_idf_comp_vfs_enabled,
80 esp_idf_vfs_support_io,
81 esp_idf_version_at_least_5_3_0
82 ))]
83 {
84 imp::restore()
85 }
86
87 #[cfg(not(all(
88 esp_idf_comp_vfs_enabled,
89 esp_idf_vfs_support_io,
90 esp_idf_version_at_least_5_3_0
91 )))]
92 {
93 false
94 }
95 }
96}
97
98mod imp {
99 use crate::*;
100
101 /// Returns `true` if the standard streams are bound to descriptors 0, 1
102 /// and 2
103 pub(super) unsafe fn bound() -> bool {
104 matches!(
105 streams(),
106 Some((si, so, se)) if fileno(si) == 0 && fileno(so) == 1 && fileno(se) == 2
107 )
108 }
109
110 #[cfg(not(esp_idf_libc_picolibc))]
111 pub(super) unsafe fn streams() -> Option<(*mut FILE, *mut FILE, *mut FILE)> {
112 // The global reentrancy structure owns the standard streams; the reent
113 // of every task points to the same streams, courtesy of `esp_reent_init`
114 let g = _global_impure_ptr;
115 if g.is_null() {
116 return None;
117 }
118
119 let (si, so, se) = ((*g)._stdin, (*g)._stdout, (*g)._stderr);
120
121 (!si.is_null() && !so.is_null() && !se.is_null()).then_some((si, so, se))
122 }
123
124 #[cfg(esp_idf_libc_picolibc)]
125 pub(super) unsafe fn streams() -> Option<(*mut FILE, *mut FILE, *mut FILE)> {
126 (!stdin.is_null() && !stdout.is_null() && !stderr.is_null())
127 .then_some((stdin, stdout, stderr))
128 }
129
130 #[cfg(all(
131 esp_idf_comp_vfs_enabled,
132 esp_idf_vfs_support_io,
133 esp_idf_version_at_least_5_3_0
134 ))]
135 pub(super) use restore::restore;
136
137 #[cfg(all(
138 esp_idf_comp_vfs_enabled,
139 esp_idf_vfs_support_io,
140 esp_idf_version_at_least_5_3_0
141 ))]
142 mod restore {
143 use core::ffi::c_int;
144 use core::ptr;
145
146 use crate::*;
147
148 use super::streams;
149
150 pub(in crate::stdio) const CONSOLE: &core::ffi::CStr = c"/dev/console";
151
152 /// Claims the lowest three free descriptors with placeholder entries of
153 /// a dummy VFS, so that the console lands on descriptors >= 3 when it
154 /// re-opens its backing devices.
155 ///
156 /// Returns the ID of the dummy VFS, to be passed to
157 /// [`release_low_fds`], or `None` if the descriptors could not be
158 /// claimed.
159 pub(in crate::stdio) unsafe fn claim_low_fds() -> Option<esp_vfs_id_t> {
160 let mut vfs_id: esp_vfs_id_t = -1;
161 let vfs = core::mem::zeroed::<esp_vfs_t>();
162
163 if esp_vfs_register_with_id(&vfs, ptr::null_mut(), &mut vfs_id) != ESP_OK {
164 return None;
165 }
166
167 for _ in 0..3 {
168 let mut placeholder: c_int = -1;
169 if esp_vfs_register_fd(vfs_id, &mut placeholder) != ESP_OK {
170 break;
171 }
172 }
173
174 Some(vfs_id)
175 }
176
177 /// Releases the placeholder descriptors claimed by [`claim_low_fds`]
178 pub(in crate::stdio) unsafe fn release_low_fds(vfs_id: esp_vfs_id_t) {
179 // (also releases the placeholder descriptors)
180 esp_vfs_unregister_with_id(vfs_id);
181 }
182
183 #[cfg(esp_idf_version_at_least_5_5_0)]
184 pub(in crate::stdio) unsafe fn init_global_stdio() {
185 esp_libc_init_global_stdio(CONSOLE.as_ptr());
186 }
187
188 #[cfg(not(esp_idf_version_at_least_5_5_0))]
189 pub(in crate::stdio) unsafe fn init_global_stdio() {
190 esp_newlib_init_global_stdio(CONSOLE.as_ptr());
191 }
192
193 /// With newlib, ESP-IDF `fopen`-s the standard streams, so the binding
194 /// is restored by tearing them down and re-creating them at a point
195 /// where descriptors 0, 1 and 2 are the lowest free ones.
196 #[cfg(not(esp_idf_libc_picolibc))]
197 pub(in crate::stdio) unsafe fn restore() -> bool {
198 let Some((si, so, se)) = streams() else {
199 return false;
200 };
201
202 // Close the standard streams. This drops the console refcount to
203 // zero, which makes the console close the descriptors of its
204 // backing devices too, so all low descriptors become free
205 fclose(si);
206 fclose(so);
207 if se != so {
208 fclose(se);
209 }
210
211 let placeholders = claim_low_fds();
212
213 // Open the console once: this makes it re-open - and re-latch - the
214 // descriptors of its backing devices, above the placeholders
215 let probe = open(CONSOLE.as_ptr(), O_WRONLY as c_int);
216
217 // Release the placeholders and re-run the stdio initialization of
218 // ESP-IDF: the standard streams now claim the freed descriptors
219 // 0, 1 and 2, in that order
220 if let Some(vfs_id) = placeholders {
221 release_low_fds(vfs_id);
222 }
223
224 init_global_stdio();
225
226 if probe >= 0 {
227 close(probe);
228 }
229
230 // Re-point the streams of the current task's reent to the
231 // re-created global ones (`esp_reent_init` had copied the old, now
232 // stale, pointers)
233 let g = _global_impure_ptr;
234 let r = __getreent();
235 if !g.is_null() && !r.is_null() && r != g {
236 (*r)._stdin = (*g)._stdin;
237 (*r)._stdout = (*g)._stdout;
238 (*r)._stderr = (*g)._stderr;
239 }
240
241 matches!(streams(), Some((_, so, _)) if fileno(so) == 1)
242 }
243
244 /// With picolibc, the standard streams are statically allocated `FILE`s
245 /// owned by ESP-IDF which cannot be torn down and re-created (see the
246 /// module documentation). Only their console handles are re-opened
247 /// here; descriptors 0, 1 and 2 are then claimed by console handles of
248 /// our own, which is all the Rust Standard Library needs.
249 #[cfg(esp_idf_libc_picolibc)]
250 pub(in crate::stdio) unsafe fn restore() -> bool {
251 use core::sync::atomic::{AtomicU8, Ordering};
252
253 const UNTRIED: u8 = 0;
254 const FAILED: u8 = 1;
255 const RESTORED: u8 = 2;
256
257 // The restoration leaves the standard streams on descriptors >= 3,
258 // so - unlike with newlib - `bound()` cannot detect a second call
259 static STATE: AtomicU8 = AtomicU8::new(UNTRIED);
260
261 match STATE.load(Ordering::Relaxed) {
262 FAILED => return false,
263 RESTORED => return true,
264 _ => (),
265 }
266
267 let Some((si, so, _)) = streams() else {
268 STATE.store(FAILED, Ordering::Relaxed);
269 return false;
270 };
271
272 // `stderr` shares the stream - and thus the descriptor - of `stdout`
273 let (sifd, sofd) = (fileno(si), fileno(so));
274
275 fflush(so);
276
277 // Close the console handles of the standard streams *without*
278 // closing the streams themselves. This drops the console refcount
279 // to zero, which makes the console close the descriptors of its
280 // backing devices too, so all low descriptors become free
281 if sifd >= 0 {
282 close(sifd);
283 }
284 if sofd >= 0 && sofd != sifd {
285 close(sofd);
286 }
287
288 let placeholders = claim_low_fds();
289
290 // Open the console once: this makes it re-open - and re-latch - the
291 // descriptors of its backing devices, above the placeholders
292 let probe = open(CONSOLE.as_ptr(), O_WRONLY as c_int);
293
294 // Re-attach the standard streams to the console *while* the
295 // placeholders are still in place: all the ESP-IDF stdio
296 // initialization does under picolibc is re-opening the console and
297 // storing the descriptors in the (static) streams - and it asserts
298 // that the `stdin` descriptor is not 0
299 init_global_stdio();
300
301 if let Some(vfs_id) = placeholders {
302 release_low_fds(vfs_id);
303 }
304
305 // Descriptors 0, 1 and 2 are only needed by users of the raw POSIX
306 // descriptors (the Rust Standard Library among them), so claim them
307 // with console handles of their own. These are deliberately never
308 // closed - they are the process' standard descriptors
309 let i = open(CONSOLE.as_ptr(), O_RDONLY as c_int);
310 let o = open(CONSOLE.as_ptr(), O_WRONLY as c_int);
311 let e = open(CONSOLE.as_ptr(), O_WRONLY as c_int);
312
313 if probe >= 0 {
314 close(probe);
315 }
316
317 let restored = i == 0 && o == 1 && e == 2;
318
319 if !restored {
320 // Do not leak the handles which did not land where they were
321 // supposed to
322 for fd in [i, o, e] {
323 if fd >= 3 {
324 close(fd);
325 }
326 }
327 }
328
329 // The streams were writing to closed descriptors for the duration
330 // of the shuffle above, which may have latched their error flag
331 clearerr(si);
332 clearerr(so);
333
334 STATE.store(if restored { RESTORED } else { FAILED }, Ordering::Relaxed);
335
336 restored
337 }
338 }
339}