Refactor: extract platform abstraction for subprocess spawning

Move all Unix-specific syscalls (fork, execvp, waitpid, strerror, /tmp)
into a platform layer so the same spectrogram.c can target Windows
(CreateProcess) and WebAssembly (stubs). Key changes:

- src/platform.h: public API with spawn handle, error codes, path helpers
- src/platform_linux.c: fork/execvp implementation, /tmp, strerror
- src/platform_win32.c: CreateProcess implementation, temp dir lookup
- src/platform_web.c: stub implementations (no subprocess support on web)
- src/spectrogram.c: consume only platform.h; replace pid_t/fork/execvp
  with Platform_SpawnChild/WaitForChild; use Platform_GetTempDir() for
  /tmp path; replace strdup with malloc/memcpy for portability
- Build: add platform_*.c to gmake, Premake, and build_web.sh
This commit is contained in:
2026-05-14 18:16:21 -07:00
parent e42554e6fd
commit b6942d8577
8 changed files with 472 additions and 28 deletions
+13
View File
@@ -202,6 +202,19 @@ if (downloadRaylib) then
files {"../src/*.rc", "../src/*.ico"} files {"../src/*.rc", "../src/*.ico"}
files {"../resources/**"} files {"../resources/**"}
-- Exclude platform-specific files on each target
filter {"system:linux"}
excludedfiles { "../src/platform_win32.c", "../src/platform_web.c" }
filter {"system:windows"}
excludedfiles { "../src/platform_linux.c", "../src/platform_web.c" }
filter {"system:macosx"}
excludedfiles { "../src/platform_win32.c", "../src/platform_web.c" }
filter {"action:emscripten"}
excludedfiles { "../src/platform_linux.c", "../src/platform_win32.c" }
filter{} filter{}
includedirs { "../src" } includedirs { "../src" }
+16 -2
View File
@@ -42,9 +42,22 @@ cd "$RAYLIB_BUILD"
emar rcs libraylib.a rcore.o rshapes.o rtextures.o rtext.o rmodels.o raudio.o emar rcs libraylib.a rcore.o rshapes.o rtextures.o rtext.o rmodels.o raudio.o
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
echo "=== Step 2: Compiling spectrogram for web ===" # Compile spectrogram (web version — no subprocess support)
# platform_web.c provides stub implementations for spawn/error functions
echo "=== Step 2: Compiling spectrogram + platform for web ==="
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
# Compile platform_web.o (handles fork/execvp stubs for web)
emcc -c -Os -Wall -DPLATFORM_WEB "$SRC_DIR/platform_web.c" -o "$RAYLIB_BUILD/platform_web.o"
# Compile spectrogram
emcc -c -Os -Wall -DPLATFORM_WEB "$SRC_DIR/spectrogram.c" \
-I"$RAYLIB_DIR/src" \
-I"$RAYLIB_DIR/src/external" \
-I"$RAYLIB_DIR/src/external/glfw/include" \
-Iinclude \
-o "$RAYLIB_BUILD/spectrogram.o"
# Linker flags # Linker flags
LDFLAGS="-s USE_GLFW=3 -s ASYNCIFY -s TOTAL_MEMORY=67108864 -s FORCE_FILESYSTEM=1 --preload-file resources@resources --preload-file fonts/DejaVuSansMono.ttf@fonts/DejaVuSansMono.ttf --shell-file $SCRIPT_DIR/web_shell.html -s NO_EXIT_RUNTIME=1" LDFLAGS="-s USE_GLFW=3 -s ASYNCIFY -s TOTAL_MEMORY=67108864 -s FORCE_FILESYSTEM=1 --preload-file resources@resources --preload-file fonts/DejaVuSansMono.ttf@fonts/DejaVuSansMono.ttf --shell-file $SCRIPT_DIR/web_shell.html -s NO_EXIT_RUNTIME=1"
@@ -56,7 +69,8 @@ fi
# Compile and link # Compile and link
emcc -o "$OUTPUT" \ emcc -o "$OUTPUT" \
"$SRC_DIR/spectrogram.c" \ "$RAYLIB_BUILD/platform_web.o" \
"$RAYLIB_BUILD/spectrogram.o" \
-I"$RAYLIB_DIR/src" \ -I"$RAYLIB_DIR/src" \
-I"$RAYLIB_DIR/src/external" \ -I"$RAYLIB_DIR/src/external" \
-I"$RAYLIB_DIR/src/external/glfw/include" \ -I"$RAYLIB_DIR/src/external/glfw/include" \
+6
View File
@@ -119,7 +119,9 @@ GENERATED :=
OBJECTS := OBJECTS :=
GENERATED += $(OBJDIR)/spectrogram.o GENERATED += $(OBJDIR)/spectrogram.o
GENERATED += $(OBJDIR)/platform_linux.o
OBJECTS += $(OBJDIR)/spectrogram.o OBJECTS += $(OBJDIR)/spectrogram.o
OBJECTS += $(OBJDIR)/platform_linux.o
# Rules # Rules
# ############################################# # #############################################
@@ -187,6 +189,10 @@ $(OBJDIR)/spectrogram.o: src/spectrogram.c
@echo "$(notdir $<)" @echo "$(notdir $<)"
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" $(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
$(OBJDIR)/platform_linux.o: src/platform_linux.c
@echo "$(notdir $<)"
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
-include $(OBJECTS:%.o=%.d) -include $(OBJECTS:%.o=%.d)
ifneq (,$(PCH)) ifneq (,$(PCH))
-include $(PCH_PLACEHOLDER).d -include $(PCH_PLACEHOLDER).d
+96
View File
@@ -0,0 +1,96 @@
#pragma once
#include <stddef.h>
/* ── Public API ─────────────────────────────────────────────────────────── */
/**
* Platform identification (for build-time selection).
* Only one should be defined: PLATFORM_LINUX, PLATFORM_WIN32, PLATFORM_WEB.
* Undefined means unknown / desktop fallback.
*/
enum Platform {
PLATFORM_UNKNOWN = 0,
PLATFORM_LINUX,
PLATFORM_WIN32,
PLATFORM_WEB,
};
/**
* Error codes returned by platform functions.
*/
enum PlatformError {
PLATFORM_OK = 0, /**< Success */
PLATFORM_GENERAL = -1, /**< Non-specific failure */
PLATFORM_NOT_SUPPORTED = -2, /**< Function not available on this platform */
};
typedef int PlatformError;
/* ── Process / spawning ─────────────────────────────────────────────────── */
/**
* Result of waiting for a spawned process.
*/
enum SpawnStatus {
SPAWN_WAITING, /**< Process still running */
SPAWN_EXITED, /**< Process has exited, exitCode is valid */
SPAWN_EXIT_CODE_UNKNOWN, /**< Exited but we could not determine code */
};
typedef int SpawnStatus;
/**
* Opaque handle to a spawned child process.
*/
typedef struct {
void *handle;
enum SpawnStatus status;
int exit_code;
int last_error;
} PlatformSpawnHandle;
/**
* Spawn a child process synchronously. Blocks until the child
* exits — without going through a shell, so user-controlled
* arguments cannot inject commands.
*
* @param program Program to run (passed directly to execvp /
* CreateProcess, never split by a shell).
* @param argv NULL-terminated argument array. argv[0] is the
* program name, argv[1..n] are arguments.
* @param[out] out_handle Fill with a handle for status queries.
* @return PLATFORM_OK on success, PLATFORM_GENERAL on failure.
*/
PlatformError Platform_SpawnChild(const char *program, const char **argv,
PlatformSpawnHandle *out_handle);
/**
* Wait for a spawned process to exit.
*/
PlatformError Platform_WaitForChild(PlatformSpawnHandle *handle);
/**
* Get the exit status of a spawned process.
*/
SpawnStatus Platform_GetExitStatus(const PlatformSpawnHandle *handle,
int *code);
/**
* Clean up resources associated with a spawn handle.
*/
void Platform_CloseSpawnHandle(PlatformSpawnHandle *handle);
/* ── Error description ─────────────────────────────────────────────────── */
/**
* Human-readable description of the last platform error.
* Returns a static string (caller does not free).
*/
const char *Platform_GetLastErrorMessage(void);
/* ── Paths ──────────────────────────────────────────────────────────────── */
/**
* Temporary directory for the platform.
* Static string — do not modify or free.
*/
const char *Platform_GetTempDir(void);
+102
View File
@@ -0,0 +1,102 @@
/* Platform layer: Linux — fork/execvp, path helpers, error strings */
#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif
#include "platform.h"
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#define STATIC static
STATIC char last_error_msg[256] = { 0 };
STATIC PlatformSpawnHandle current_handle = { 0 };
/* ── Helpers ─────────────────────────────────────────────────────────── */
STATIC void set_last_error(const char *fmt, ...) {
// Simple snprintf to a local buffer — fine for error paths
char buf[256] = { 0 };
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
strncpy(last_error_msg, buf, sizeof(last_error_msg) - 1);
}
/* ── Platform API ────────────────────────────────────────────────────── */
PlatformError Platform_SpawnChild(const char *program, const char **argv,
PlatformSpawnHandle *out_handle) {
last_error_msg[0] = '\0';
memset(out_handle, 0, sizeof(*out_handle));
pid_t pid = fork();
if (pid < 0) {
snprintf(last_error_msg, sizeof(last_error_msg),
"fork() failed: %s", strerror(errno));
out_handle->last_error = errno;
return PLATFORM_GENERAL;
}
if (pid == 0) {
// Child process — replace with target
execvp(program, (char **)argv);
_exit(127); // execvp only returns on error
}
// Parent
out_handle->handle = (void *)(intptr_t)pid;
out_handle->status = SPAWN_WAITING;
return PLATFORM_OK;
}
PlatformError Platform_WaitForChild(PlatformSpawnHandle *handle) {
if (!handle || !handle->handle) return PLATFORM_GENERAL;
pid_t pid = (pid_t)(intptr_t)handle->handle;
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
handle->status = SPAWN_EXITED;
handle->exit_code = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
handle->status = SPAWN_EXITED;
handle->exit_code = WTERMSIG(status);
} else {
handle->status = SPAWN_EXIT_CODE_UNKNOWN;
}
handle->handle = NULL; // Consumed
return PLATFORM_OK;
}
SpawnStatus Platform_GetExitStatus(const PlatformSpawnHandle *handle, int *code) {
if (!handle) return SPAWN_WAITING;
if (code) *code = handle->exit_code;
return handle->status;
}
void Platform_CloseSpawnHandle(PlatformSpawnHandle *handle) {
if (!handle) return;
handle->handle = NULL;
handle->status = SPAWN_WAITING;
handle->exit_code = 0;
handle->last_error = 0;
}
const char *Platform_GetLastErrorMessage(void) {
return last_error_msg;
}
const char *Platform_GetTempDir(void) {
return "/tmp";
}
+65
View File
@@ -0,0 +1,65 @@
/*
* platform_web.c — Emscripten/WebAssembly stub implementation
*
* The web version cannot spawn child processes, so these functions
* return "not supported". A real implementation would use Emscripten's
* pthreads or asyncify if ffmpeg integration is needed.
*/
#include "platform.h"
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
/* ── Helpers ─────────────────────────────────────────────────────────── */
static char last_error_msg[256] = { 0 };
static void set_last_error(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vsnprintf(last_error_msg, sizeof(last_error_msg), fmt, args);
va_end(args);
}
/* ── Platform API ────────────────────────────────────────────────────── */
PlatformError Platform_SpawnChild(const char *program, const char **argv,
PlatformSpawnHandle *out_handle) {
(void)program;
(void)argv;
set_last_error("Spawning child processes is not supported on the web");
out_handle->last_error = ENOSYS;
return PLATFORM_NOT_SUPPORTED;
}
PlatformError Platform_WaitForChild(PlatformSpawnHandle *handle) {
(void)handle;
return PLATFORM_NOT_SUPPORTED;
}
SpawnStatus Platform_GetExitStatus(const PlatformSpawnHandle *handle, int *code) {
(void)handle;
(void)code;
return SPAWN_WAITING;
}
void Platform_CloseSpawnHandle(PlatformSpawnHandle *handle) {
if (!handle) return;
handle->handle = NULL;
handle->status = SPAWN_WAITING;
handle->exit_code = 0;
handle->last_error = 0;
}
const char *Platform_GetLastErrorMessage(void) {
return last_error_msg;
}
const char *Platform_GetTempDir(void) {
/*
* Emscripten's virtual filesystem: "/" works as root.
* For persistent downloads/uploads, consider IDBFS mount.
*/
return "/";
}
+129
View File
@@ -0,0 +1,129 @@
/*
* platform_win32.c — Windows implementation
*
* Requires: Windows.h, mincore.lib (for CreateProcessA)
*/
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "platform.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
/* ── Helpers ─────────────────────────────────────────────────────────── */
static char last_error_msg[256] = { 0 };
static void set_last_error(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vsnprintf(last_error_msg, sizeof(last_error_msg), fmt, args);
va_end(args);
}
/* ── Platform API ────────────────────────────────────────────────────── */
PlatformError Platform_SpawnChild(const char *program, const char **argv,
PlatformSpawnHandle *out_handle) {
last_error_msg[0] = '\0';
/* Build a single command line from argv */
char cmdline[2048] = { 0 };
for (int i = 0; argv[i]; i++) {
if (i > 0) strncat(cmdline, " ", sizeof(cmdline) - strlen(cmdline) - 1);
/* Quote arguments that contain spaces */
if (strchr(argv[i], ' ')) {
strncat(cmdline, "\"", sizeof(cmdline) - strlen(cmdline) - 1);
strncat(cmdline, argv[i], sizeof(cmdline) - strlen(cmdline) - 1);
strncat(cmdline, "\"", sizeof(cmdline) - strlen(cmdline) - 1);
} else {
strncat(cmdline, argv[i], sizeof(cmdline) - strlen(cmdline) - 1);
}
}
/* CreateProcess needs a mutable buffer */
char cmd_copy[2048];
strncpy(cmd_copy, cmdline, sizeof(cmd_copy) - 1);
cmd_copy[sizeof(cmd_copy) - 1] = '\0';
STARTUPINFOA si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcessA(NULL, cmd_copy, NULL, NULL, FALSE,
0, NULL, NULL, &si, &pi)) {
DWORD err = GetLastError();
char msg[256];
sprintf(msg, "CreateProcess failed (code %lu)", err);
strncpy(last_error_msg, msg, sizeof(last_error_msg) - 1);
out_handle->last_error = (int)err;
return PLATFORM_GENERAL;
}
out_handle->handle = (void *)pi.hProcess;
out_handle->status = SPAWN_WAITING;
/* Don't close the thread handle here — CloseSpawnHandle will */
CloseHandle(pi.hThread);
return PLATFORM_OK;
}
PlatformError Platform_WaitForChild(PlatformSpawnHandle *handle) {
if (!handle || !handle->handle) return PLATFORM_GENERAL;
DWORD wait_result = WaitForSingleObject((HANDLE)handle->handle, INFINITE);
if (wait_result != WAIT_OBJECT_0) {
handle->last_error = (int)GetLastError();
return PLATFORM_GENERAL;
}
DWORD exit_code = 0;
if (!GetExitCodeProcess((HANDLE)handle->handle, &exit_code)) {
handle->status = SPAWN_EXIT_CODE_UNKNOWN;
handle->last_error = (int)GetLastError();
return PLATFORM_GENERAL;
}
handle->status = SPAWN_EXITED;
handle->exit_code = (int)exit_code;
handle->handle = NULL;
return PLATFORM_OK;
}
SpawnStatus Platform_GetExitStatus(const PlatformSpawnHandle *handle, int *code) {
if (!handle) return SPAWN_WAITING;
if (code) *code = handle->exit_code;
return handle->status;
}
void Platform_CloseSpawnHandle(PlatformSpawnHandle *handle) {
if (!handle) return;
if (handle->handle) {
CloseHandle((HANDLE)handle->handle);
handle->handle = NULL;
}
handle->status = SPAWN_WAITING;
handle->exit_code = 0;
handle->last_error = 0;
}
const char *Platform_GetLastErrorMessage(void) {
return last_error_msg;
}
const char *Platform_GetTempDir(void) {
/* Get temp directory path once and cache it */
static char temp_path[MAX_PATH] = { 0 };
if (temp_path[0] == '\0') {
DWORD len = GetTempPathA(sizeof(temp_path), temp_path);
if (len == 0 || len >= sizeof(temp_path)) {
strncpy(temp_path, "C:\\Windows\\Temp", sizeof(temp_path) - 1);
}
}
return temp_path;
}
+38 -19
View File
@@ -1,11 +1,9 @@
// spectrogram.c - Spectrogram viewer with region selection and playback // spectrogram.c - Spectrogram viewer with region selection and playback
// Based on Unity Audio-Experiments project // Based on Unity Audio-Experiments project
#define _POSIX_C_SOURCE 200809L
#define _DEFAULT_SOURCE
#include "raylib.h" #include "raylib.h"
#include "resource_dir.h" #include "resource_dir.h"
#include "platform.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -418,21 +416,34 @@ static void FreeSTFT(StftResult* result)
// Convert audio file to WAV using ffmpeg (if available) // Convert audio file to WAV using ffmpeg (if available)
static bool ConvertToFFmpegWAV(const char* inputPath, char* outputPath, size_t outputSize) static bool ConvertToFFmpegWAV(const char* inputPath, char* outputPath, size_t outputSize)
{ {
// Create temp WAV path snprintf(outputPath, outputSize, "%s/rspektrum_temp_converted.wav",
snprintf(outputPath, outputSize, "/tmp/rspektrum_temp_converted.wav"); Platform_GetTempDir());
// Build ffmpeg command /* Build argv array — no shell = no injection risk */
char cmd[1024]; const char* argv[] = {
snprintf(cmd, sizeof(cmd), "ffmpeg -y -loglevel quiet -i \"%s\" -ar 48000 -ac 1 -f wav \"%s\" 2>&1", inputPath, outputPath); "ffmpeg", "-y", "-loglevel", "quiet",
"-i", inputPath,
"-ar", "48000", "-ac", "1", "-f", "wav",
outputPath, NULL
};
int result = system(cmd); PlatformSpawnHandle handle = { 0 };
PlatformError rc = Platform_SpawnChild("ffmpeg", argv, &handle);
if (rc != PLATFORM_OK) {
TraceLog(LOG_WARNING, "Failed to spawn ffmpeg: %s", Platform_GetLastErrorMessage());
return false;
}
if (result == 0 && FileExists(outputPath)) { Platform_WaitForChild(&handle);
int exit_code = 0;
SpawnStatus status = Platform_GetExitStatus(&handle, &exit_code);
if (status == SPAWN_EXITED && exit_code == 0 && FileExists(outputPath)) {
TraceLog(LOG_INFO, "FFmpeg conversion successful: %s", outputPath); TraceLog(LOG_INFO, "FFmpeg conversion successful: %s", outputPath);
return true; return true;
} }
TraceLog(LOG_WARNING, "FFmpeg conversion failed or not available"); TraceLog(LOG_WARNING, "FFmpeg conversion failed (exit code %d)", exit_code);
return false; return false;
} }
@@ -459,7 +470,7 @@ static bool LoadWavFile(const char* filepath, AudioSignal* signal)
Wave wave = LoadWave(filepath); Wave wave = LoadWave(filepath);
if (wave.data == NULL) { if (wave.data == NULL) {
TraceLog(LOG_ERROR, "Failed to open WAV file: %s", filepath); TraceLog(LOG_ERROR, "Failed to open WAV file: %s", filepath);
if (isConverted) remove(convertedPath); if (isConverted) FileRemove(convertedPath);
return false; return false;
} }
@@ -493,7 +504,7 @@ static bool LoadWavFile(const char* filepath, AudioSignal* signal)
// Clean up temp file if converted // Clean up temp file if converted
if (isConverted) { if (isConverted) {
remove(convertedPath); FileRemove(convertedPath);
TraceLog(LOG_INFO, "Cleaned up temp file: %s", convertedPath); TraceLog(LOG_INFO, "Cleaned up temp file: %s", convertedPath);
} }
@@ -768,9 +779,13 @@ static void ScanDirectory(const char* path)
const char* name = GetFileName(files.paths[i]); const char* name = GetFileName(files.paths[i]);
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
if (DirectoryExists(files.paths[i])) { if (DirectoryExists(files.paths[i])) {
app.browserFiles[app.browserFileCount] = strdup(name); size_t len = strlen(name) + 1;
app.browserIsDir[app.browserFileCount] = true; app.browserFiles[app.browserFileCount] = (char*)malloc(len);
app.browserFileCount++; if (app.browserFiles[app.browserFileCount]) {
memcpy(app.browserFiles[app.browserFileCount], name, len);
app.browserIsDir[app.browserFileCount] = true;
app.browserFileCount++;
}
} }
} }
for (int i = 0; i < files.count; i++) { for (int i = 0; i < files.count; i++) {
@@ -780,9 +795,13 @@ static void ScanDirectory(const char* path)
const char* ext = GetFileExtension(files.paths[i]); const char* ext = GetFileExtension(files.paths[i]);
if (ext && (strcmp(ext, ".wav") == 0 || strcmp(ext, ".WAV") == 0 || if (ext && (strcmp(ext, ".wav") == 0 || strcmp(ext, ".WAV") == 0 ||
strcmp(ext, ".Wave") == 0 || strcmp(ext, ".Wav") == 0)) { strcmp(ext, ".Wave") == 0 || strcmp(ext, ".Wav") == 0)) {
app.browserFiles[app.browserFileCount] = strdup(name); size_t len = strlen(name) + 1;
app.browserIsDir[app.browserFileCount] = false; app.browserFiles[app.browserFileCount] = (char*)malloc(len);
app.browserFileCount++; if (app.browserFiles[app.browserFileCount]) {
memcpy(app.browserFiles[app.browserFileCount], name, len);
app.browserIsDir[app.browserFileCount] = false;
app.browserFileCount++;
}
} }
} }
} }