Compare commits

...

3 Commits

Author SHA1 Message Date
Ken Van Hoeylandt
9be33a3ebe Moved stdio wrapping to app-module
app-module is the owning module for this feature
2026-09-18 01:11:28 +02:00
Ken Van Hoeylandt
4ce62e7a87 Fix for macOS symbol wrapping 2026-09-18 00:54:53 +02:00
Ken Van Hoeylandt
a03daa4675 Fix for warnings due to wrapping issue on macOS 2026-09-18 00:43:23 +02:00
9 changed files with 166 additions and 91 deletions

View File

@ -12,13 +12,26 @@ tactility_add_module(app-module
PRIV_REQUIRES TactilityKernelCpp
)
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close()
# rather than calling ::read/::write/::close() directly, on every platform whose build wraps
# those symbols (ESP-IDF always; POSIX except macOS, whose linker doesn't support --wrap) -
# see Tactility/CMakeLists.txt and the top-level CMakeLists.txt for where that's applied.
if (NOT APPLE)
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close() rather
# than calling ::read/::write/::close() directly, on every platform where those are wrapped: via
# -Wl,--wrap= on ESP-IDF (see the top-level CMakeLists.txt) and non-Apple POSIX (below), or via
# source/stdio_wrap.cpp's dyld interpose on Apple (whose linker lacks --wrap).
tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
# Routes every read()/write()/close() and printf-family call to source/stdio_wrap.cpp's __wrap_*
# functions; see that file for what they do and why. ESP-IDF wraps the same symbols itself (see
# the top-level CMakeLists.txt); Apple gets the same routing via dyld interpose instead, self-
# registered in that file. PUBLIC so every consumer of this module (Tactility, and any test target
# that links app-module directly) gets the flags too.
if (NOT ESP_PLATFORM AND NOT APPLE)
tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
target_link_options(${MODULE_NAME} PUBLIC
"-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
)
endif ()
# install.cpp resolves an installed binary's fixed bin/<platform>/<binary>.so path itself, so it

View File

@ -1,9 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
// Paired with -Wl,--wrap=read/write/close - see Tactility/CMakeLists.txt (POSIX) and the
// top-level CMakeLists.txt (ESP32) for where that's applied. On a platform where it isn't
// (currently: macOS, whose linker doesn't support --wrap), these are simply never called - real
// read()/write()/close() calls go straight through unredirected.
// Paired with -Wl,--wrap=read/write/close on POSIX (this module's own CMakeLists.txt) and ESP32
// (top-level CMakeLists.txt); self-registered via dyld interpose below on Apple, whose linker
// doesn't support --wrap.
#include <app/io.h>
#include <sys/types.h>
@ -24,21 +23,64 @@ int __wrap_close(int fd) {
}
#ifdef __APPLE__
// --wrap also synthesizes __real_read/write/close automatically; dyld interpose doesn't, so
// io.cpp's TT_APP_IO_WRAPS_STDIO fallback needs them defined here. dlsym(RTLD_NEXT, ...) is the
// standard way to reach the true libSystem implementation despite the interpose below: a direct
// call to read/write/close from this file would just recurse into __wrap_read/write/close, since
// interpose rewrites every reference to those symbols in the process, this file included.
#include <dlfcn.h>
extern "C" {
ssize_t __real_read(int fd, void* buffer, size_t size) {
static auto real = reinterpret_cast<ssize_t (*)(int, void*, size_t)>(dlsym(RTLD_NEXT, "read"));
return real(fd, buffer, size);
}
ssize_t __real_write(int fd, const void* buffer, size_t size) {
static auto real = reinterpret_cast<ssize_t (*)(int, const void*, size_t)>(dlsym(RTLD_NEXT, "write"));
return real(fd, buffer, size);
}
int __real_close(int fd) {
static auto real = reinterpret_cast<int (*)(int)>(dlsym(RTLD_NEXT, "close"));
return real(fd);
}
}
// <mach-o/dyld-interposing.h> isn't a public SDK header (it ships with dyld's own source, not
// Xcode/Command Line Tools), so this reimplements its DYLD_INTERPOSE macro locally; reused below
// for the printf-family interposes too.
#define TT_DYLD_INTERPOSE(replacement, replacee) \
__attribute__((used)) static struct { const void* replacement; const void* replacee; } \
tt_interpose_##replacee __attribute__((section("__DATA,__interpose"))) = { \
(const void*)(unsigned long)&(replacement), (const void*)(unsigned long)&(replacee) \
};
TT_DYLD_INTERPOSE(__wrap_read, read)
TT_DYLD_INTERPOSE(__wrap_write, write)
TT_DYLD_INTERPOSE(__wrap_close, close)
#endif // __APPLE__
// region glibc stdio wraps
//
// glibc's printf/fprintf/etc are compiled into libc.so and call an internal, non-exported write()
// alias - --wrap=write (above) can't reach that call, only calls WE make to the public symbol.
// These wraps instead redirect calls WE make to printf/fprintf/etc, the same trick as read/write/
// close above. Newlib (ESP-IDF) doesn't have this gap - its stdio does call the wrappable syscall
// stubs - so Tactility/CMakeLists.txt only applies the matching -Wl,--wrap= flags on POSIX.
// libc's printf/fprintf/etc are compiled into the C library and call an internal, non-exported
// write() alias, which --wrap=write/the read/write/close interpose above can't reach: only calls
// WE make to the public symbol. These wraps instead redirect calls WE make to printf/fprintf/etc,
// the same trick as read/write/close above. Newlib (ESP-IDF) doesn't have this gap: its stdio does
// call the wrappable syscall stubs, so this block is POSIX-only.
//
// Scoped to the printf/getc families only: fread/fwrite take an arbitrary FILE* and are already
// used sitewide for real file I/O (e.g. File.cpp's readBinaryInternal), so wrapping them would
// route every such call through this file's stdin/stdout check - a correctness risk for unrelated
// code that isn't worth taking here. putc/getc are excluded too since glibc defines them as
// route every such call through this file's stdin/stdout check, a correctness risk for unrelated
// code that isn't worth taking here. putc/getc are excluded too since libc defines them as
// macros, not real calls, so wrapping those symbols wouldn't reliably intercept them.
#if !defined(ESP_PLATFORM) && !defined(__APPLE__)
#if !defined(ESP_PLATFORM)
#include <cstdarg>
#include <cstdio>
@ -54,6 +96,43 @@ int __real_fgetc(FILE* stream);
char* __real_fgets(char* buffer, int size, FILE* stream);
}
#ifdef __APPLE__
// --wrap synthesizes these automatically elsewhere; on Apple they're defined here via
// dlsym(RTLD_NEXT, ...) instead. See the read/write/close __real_* block above for why.
#include <dlfcn.h>
extern "C" {
int __real_vfprintf(FILE* stream, const char* format, va_list args) {
static auto real = reinterpret_cast<int (*)(FILE*, const char*, va_list)>(dlsym(RTLD_NEXT, "vfprintf"));
return real(stream, format, args);
}
int __real_fputs(const char* s, FILE* stream) {
static auto real = reinterpret_cast<int (*)(const char*, FILE*)>(dlsym(RTLD_NEXT, "fputs"));
return real(s, stream);
}
int __real_fputc(int c, FILE* stream) {
static auto real = reinterpret_cast<int (*)(int, FILE*)>(dlsym(RTLD_NEXT, "fputc"));
return real(c, stream);
}
int __real_fgetc(FILE* stream) {
static auto real = reinterpret_cast<int (*)(FILE*)>(dlsym(RTLD_NEXT, "fgetc"));
return real(stream);
}
char* __real_fgets(char* buffer, int size, FILE* stream) {
static auto real = reinterpret_cast<char* (*)(char*, int, FILE*)>(dlsym(RTLD_NEXT, "fgets"));
return real(buffer, size, stream);
}
}
#endif // __APPLE__
namespace {
void writeAllToStdout(const void* data, size_t size) {
@ -195,6 +274,20 @@ char* __wrap_fgets(char* buffer, int size, FILE* stream) {
}
#endif // !ESP_PLATFORM && !__APPLE__
#ifdef __APPLE__
TT_DYLD_INTERPOSE(__wrap_vprintf, vprintf)
TT_DYLD_INTERPOSE(__wrap_printf, printf)
TT_DYLD_INTERPOSE(__wrap_vfprintf, vfprintf)
TT_DYLD_INTERPOSE(__wrap_fprintf, fprintf)
TT_DYLD_INTERPOSE(__wrap_puts, puts)
TT_DYLD_INTERPOSE(__wrap_fputs, fputs)
TT_DYLD_INTERPOSE(__wrap_putchar, putchar)
TT_DYLD_INTERPOSE(__wrap_fputc, fputc)
TT_DYLD_INTERPOSE(__wrap_getchar, getchar)
TT_DYLD_INTERPOSE(__wrap_fgetc, fgetc)
TT_DYLD_INTERPOSE(__wrap_fgets, fgets)
#endif // __APPLE__
#endif // !ESP_PLATFORM
// endregion

View File

@ -6,30 +6,12 @@ enable_language(C CXX ASM)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(AppModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
if (NOT APPLE)
# Provides __wrap_read/write/close for the -Wl,--wrap= below (see Tactility/CMakeLists.txt
# for the canonical pairing of this file with those flags).
target_sources(AppModuleTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../../Tactility/Source/AppStdioWrap.cpp)
endif ()
target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC} ${CMAKE_CURRENT_LIST_DIR}/../private)
add_test(NAME AppModuleTests COMMAND AppModuleTests)
# Matches app-module's own TT_APP_IO_WRAPS_STDIO (see Modules/app-module/CMakeLists.txt):
# io.cpp calls __real_read/write/close() on non-Apple POSIX, so any final link of it needs
# these wraps too, or those symbols go unresolved. The printf-family/getc-family flags are
# needed for the same reason: AppStdioWrap.cpp compiles those __wrap_* functions unconditionally
# on this platform (see its own #if guard), and they reference __real_vfprintf/fputs/fputc/
# fgetc/fgets - those only exist once the matching --wrap flag is passed.
if (NOT APPLE)
target_link_options(AppModuleTests PRIVATE
"-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
)
endif ()
# No --wrap flags or stdio_wrap.cpp source needed here: linking app-module below already brings
# both along (see its own CMakeLists.txt).
target_link_libraries(AppModuleTests PUBLIC
TactilityKernel

View File

@ -6,3 +6,12 @@ add_library(platform-posix OBJECT)
target_sources(platform-posix PRIVATE ${SOURCES})
#target_include_directories(platform-posix PUBLIC include/)
target_link_libraries(platform-posix PUBLIC TactilityKernel)
# Routes every pthread_attr_setstack() call to __wrap_pthread_attr_setstack() in source/pthread_stack_wrap.c,
# which no-ops it. see that file for why.
# --wrap is a GNU ld option; Apple's linker doesn't support it, so that file instead self-registers a dyld interpose
# for this symbol on Apple, needing no link flag. PUBLIC so every consumer of this OBJECT library
# (Tactility, and every test target that links platform-posix directly) gets it applied too.
if (NOT APPLE)
target_link_options(platform-posix PUBLIC "-Wl,--wrap=pthread_attr_setstack")
endif ()

View File

@ -2,20 +2,15 @@
#include <stddef.h>
/**
* Linked in with -Wl,--wrap=pthread_attr_setstack (see Tactility/CMakeLists.txt).
* FreeRTOS's POSIX port hands every task a stack carved from its own heap (pvPortMalloc) via this call.
* pthread_attr_setstack requires page alignment that pvPortMalloc doesn't guarantee.
* When it succeeds anyway, the task's real pthread stack ends up living inside that small FreeRTOS heap region,
* where a thread doing heavier stack work (e.g. Mesa GL shader compilation) can silently corrupt adjacent heap_4 objects.
* No-op'ing the call instead leaves every task's pthread_attr_t at its pthread_attr_init() default,
* so pthread_create() gives it a real, properly sized stack.
*
* FreeRTOS's POSIX port (Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/port.c)
* hands every task a stack carved out of its own heap (pvPortMalloc) via this call.
* pthread_attr_setstack requires page alignment, which pvPortMalloc doesn't guarantee;
* when it happens to succeed anyway (allocator alignment can vary run to run), the
* task's real pthread stack ends up living inside that small FreeRTOS heap region -
* fine for typical embedded task code, but a desktop GL driver doing on-the-fly shader
* compilation on that thread (e.g. Mesa on first frame present) can overflow it and
* silently corrupt adjacent heap_4 objects.
*
* Wrapping the call out entirely (rather than patching the vendored port.c) leaves every
* task's pthread_attr_t at its pthread_attr_init() default, so pthread_create() always
* gives it a real, properly allocated default-size stack instead.
* Linked via -Wl,--wrap=pthread_attr_setstack (this module's own CMakeLists.txt) on non-Apple
* platforms; reused as a dyld interpose target below on Apple platforms, whose linker lacks --wrap.
*/
int __wrap_pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize) {
(void)attr;
@ -23,3 +18,17 @@ int __wrap_pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t s
(void)stacksize;
return 0;
}
#ifdef __APPLE__
// <mach-o/dyld-interposing.h> isn't a public SDK header (it ships with dyld's own source, not
// Xcode/Command Line Tools), so this reimplements its DYLD_INTERPOSE macro locally.
#define TT_DYLD_INTERPOSE(replacement, replacee) \
__attribute__((used)) static struct { const void* replacement; const void* replacee; } \
tt_interpose_##replacee __attribute__((section("__DATA,__interpose"))) = { \
(const void*)(unsigned long)&(replacement), (const void*)(unsigned long)&(replacee) \
};
TT_DYLD_INTERPOSE(__wrap_pthread_attr_setstack, pthread_attr_setstack)
#endif

View File

@ -185,31 +185,9 @@ else ()
# Exports Tactility's own symbols (-rdynamic) so a dlopen()ed app-posix-module app can resolve
# calls back into it - the OS-native equivalent of app-esp32-module's custom symbol resolver.
set_target_properties(Tactility PROPERTIES ENABLE_EXPORTS ON)
# Routes every pthread_attr_setstack() call (FreeRTOS's POSIX port hands each task a stack
# carved out of its own heap through this) to __wrap_pthread_attr_setstack() in
# Platforms/platform-posix/source/pthread_stack_wrap.c, which no-ops it - see that file for why.
# --wrap is a GNU ld option; Apple's linker doesn't support it.
if (NOT APPLE)
target_link_options(Tactility PRIVATE "-Wl,--wrap=pthread_attr_setstack")
# Routes every read()/write()/close() call to Tactility/Source/AppStdioWrap.cpp's
# __wrap_read/write/close(), which forward into app_io_read/write/close() - the fd-table
# dispatch that lets an app's own stdio (e.g. a fileselection dialog's printf'd result
# path) reach an AppStream a parent bound via app_manager_start_with_streams(). Mirrors
# what ESP-IDF's build already does for the ESP32 target (see top-level CMakeLists.txt).
target_link_options(Tactility PRIVATE "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close")
# glibc's printf/fprintf/etc don't call the public write() symbol internally (they're
# already compiled into libc.so, out of --wrap's reach), so the read/write/close wrap
# above can't see them. Newlib (ESP-IDF) doesn't have this gap - its stdio does call the
# wrappable syscall stubs - so this block is POSIX-only. Wrapping these symbols instead
# redirects OUR OWN calls to them (the only ones --wrap can rewrite) through
# AppStdioWrap.cpp's __wrap_* functions, which check the target stream (stdout/stdin) and
# fall back to the real libc function for any other FILE*.
target_link_options(Tactility PRIVATE
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
)
endif ()
# pthread_attr_setstack and read/write/close/printf-family aren't wrapped here: linking
# platform-posix and app-module already brings those wraps along (see their own
# CMakeLists.txt files).
endif ()
#

View File

@ -85,7 +85,7 @@ int32_t appMain(int argc, char* argv[]) {
if (ctx.resultCode == 0) {
// The parent captures this via an AppStream bound to our stdout (see startWithMode()) -
// see AppStdioWrap.cpp for how printf() itself gets routed there on POSIX.
// see Modules/app-module/source/stdio_wrap.cpp for how printf() itself gets routed there on POSIX.
LOG_I(TAG, "Result: %s", ctx.resultPath.c_str());
printf("%s", ctx.resultPath.c_str());
}

View File

@ -138,7 +138,7 @@ int32_t appMain(int argc, char* argv[]) {
if (ctx.resultCode == 0) {
// The caller captures this via an AppStream bound to our stdout (see start()); see
// AppStdioWrap.cpp for how printf() itself gets routed there on POSIX.
// Modules/app-module/source/stdio_wrap.cpp for how printf() itself gets routed there on POSIX.
printf("%s", ctx.resultText.c_str());
}
return ctx.resultCode;

View File

@ -19,18 +19,9 @@ target_include_directories(TactilityTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../P
add_test(NAME TactilityTests COMMAND TactilityTests)
# Matches Tactility/CMakeLists.txt's own set of --wrap flags: this binary also compiles
# AppStdioWrap.cpp and links app-module (whose io.cpp calls __real_read/write/close() on
# non-Apple POSIX - see TT_APP_IO_WRAPS_STDIO in Modules/app-module/CMakeLists.txt), so it
# needs the same wraps applied or those go unresolved.
if (NOT APPLE)
target_link_options(TactilityTests PRIVATE
"-Wl,--wrap=pthread_attr_setstack" "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
)
endif ()
# No --wrap flags declared here: linking platform-posix and app-module below already brings all
# of pthread_attr_setstack/read/write/close/the printf family along (see their own CMakeLists.txt
# files).
target_link_libraries(TactilityTests PRIVATE
TactilityKernel