Refactor SDL/FreeRTOS implementation to support macOS simulator properly (#653)

- Run SDL from main() and place FreeRTOS in separate thread. This fixes macOS support.
- Updated GitHub Actions to publish macOS simulator build for testing, updated amd64 to x86_64 for consistent naming.
This commit is contained in:
Ken Van Hoeylandt
2026-09-20 23:12:42 +02:00
committed by Adolfo Reyna
parent 89e8baf517
commit 72089f74f3
25 changed files with 748 additions and 373 deletions
+2 -20
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
@@ -15,6 +15,7 @@
#include <tactility/delay.h>
#include <tactility/freertos/task.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
@@ -110,6 +111,11 @@ int32_t stream_writer_app_main(int, char*[]) {
return 7;
}
int32_t printf_stream_writer_app_main(int, char*[]) {
printf("loc");
return 7;
}
// Writes argv[0] to its own stdout, for the app_execute_with_streams() argv-delivery test.
int32_t argv_echo_app_main(int argc, char* argv[]) {
if (argc < 1) {
@@ -321,3 +327,53 @@ TEST_CASE("app_execute_for_result_with_streams delivers both the stream data and
app_manager_stop(parent_id);
app_manager_remove("test.app.execute.parent_streams");
}
TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() calls too") {
ensure_memory_loader_registered();
AppManifest parent_manifest { "test.app.execute.printf_parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(location_app_main) } };
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
uint32_t parent_id = 0;
REQUIRE_EQ(app_start("test.app.execute.printf_parent", 0, nullptr, &parent_id), ERROR_NONE);
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
TaskEventGroup parent_event_group {};
task_event_group_construct(&parent_event_group);
AppEventSubscription parent_sub {};
REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE);
uint8_t storage[64];
AppStream child_stdout {};
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &parent_event_group };
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(printf_stream_writer_app_main) };
uint32_t child_id = 0;
REQUIRE_EQ(app_execute_for_result_with_streams(location, AppStackConfig {}, 0, nullptr, &binding, 1, parent_id, &child_id), ERROR_NONE);
std::vector<uint8_t> received;
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {
uint8_t chunk[16];
size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk));
if (n == 0) {
break; // EOF
}
received.insert(received.end(), chunk, chunk + n);
}
REQUIRE_EQ(received.size(), 3u);
CHECK_EQ(std::memcmp(received.data(), "loc", 3), 0);
REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE);
AppEvent event {};
REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE);
CHECK_EQ(event.type, APP_EVENT_RESULT);
CHECK_EQ(event.result.launch_id, child_id);
CHECK_EQ(event.result.result, 7);
app_stream_unsubscribe(&child_stdout);
app_event_unsubscribe(&parent_sub);
task_event_group_destruct(&parent_event_group);
app_manager_stop(child_id);
app_manager_stop(parent_id);
app_manager_remove("test.app.execute.printf_parent");
}
@@ -16,6 +16,7 @@
#include <unistd.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <vector>
@@ -66,6 +67,11 @@ int32_t stdout_writer_app_main(int, char*[]) {
return 0;
}
int32_t stdout_printf_app_main(int, char*[]) {
printf("hello");
return 0;
}
std::atomic<bool> g_blocked_writer_saw_error { false };
std::atomic<bool> g_blocked_writer_done { false };
@@ -180,6 +186,41 @@ TEST_CASE("app_start_with_streams pipes a child's app_io_write() calls into a pa
app_manager_remove("test.io.writer");
}
TEST_CASE("app_start_with_streams pipes a child's plain printf() calls into a parent-owned AppStream") {
ensure_memory_loader_registered();
AppManifest manifest { "test.io.printf_writer", "PrintfWriter", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(stdout_printf_app_main) } };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
uint8_t storage[64];
AppStream child_stdout {};
AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group };
AppInstanceId child_id = 0;
REQUIRE_EQ(app_start_with_streams("test.io.printf_writer", &binding, 1, &child_id), ERROR_NONE);
std::vector<uint8_t> received;
while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) {
uint8_t chunk[16];
size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk));
if (n == 0) {
break; // EOF
}
received.insert(received.end(), chunk, chunk + n);
}
REQUIRE_EQ(received.size(), 5u);
CHECK_EQ(std::memcmp(received.data(), "hello", 5), 0);
REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000));
app_stream_unsubscribe(&child_stdout);
task_event_group_destruct(&event_group);
app_manager_remove("test.io.printf_writer");
}
TEST_CASE("a write blocked on a full stream wakes with an error once the consumer closes it") {
ensure_memory_loader_registered();
g_blocked_writer_saw_error.store(false, std::memory_order_relaxed);