Skip to main content

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}