Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4000,6 +4000,9 @@ if(SDL_SHARED)
if(NOT CMAKE_VERSION VERSION_LESS "3.16")
target_precompile_headers(SDL3-shared PRIVATE "$<$<OR:$<COMPILE_LANGUAGE:C>,$<COMPILE_LANGUAGE:CXX>>:${PROJECT_SOURCE_DIR}/src/SDL_internal.h>")
endif()
if(HAIKU)
target_link_libraries(SDL3-shared PUBLIC network)
endif()
endif()

if(SDL_STATIC)
Expand All @@ -4024,6 +4027,9 @@ if(SDL_STATIC)
if(NOT CMAKE_VERSION VERSION_LESS "3.16")
target_precompile_headers(SDL3-static PRIVATE "$<$<OR:$<COMPILE_LANGUAGE:C>,$<COMPILE_LANGUAGE:CXX>>:${PROJECT_SOURCE_DIR}/src/SDL_internal.h>")
endif()
if(HAIKU)
target_link_libraries(SDL3-static PUBLIC network)
endif()
endif()

sdl_compile_definitions(
Expand Down Expand Up @@ -4055,6 +4061,9 @@ if(SDL_TEST_LIBRARY)
target_link_libraries(SDL3_test PRIVATE ${EXTRA_TEST_LIBS})
set_property(TARGET SDL3_test APPEND PROPERTY COMPATIBLE_INTERFACE_STRING "SDL_VERSION")
set_property(TARGET SDL3_test PROPERTY INTERFACE_SDL_VERSION "SDL${SDL3_VERSION_MAJOR}")
if(HAIKU)
target_link_libraries(SDL3_test PUBLIC network)
endif()
endif()

##### Configure installation folders #####
Expand Down
30 changes: 30 additions & 0 deletions include/SDL3/SDL_process.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ extern "C" {
*/
typedef struct SDL_Process SDL_Process;

/**
* An opaque handle representing an IPC channel.
*/
typedef struct SDL_IPC SDL_IPC;

/**
* Create a new process.
*
Expand Down Expand Up @@ -201,6 +206,8 @@ typedef enum SDL_ProcessIO
* property is only important if you want to start programs that does
* non-standard command-line processing, and in most cases using
* `SDL_PROP_PROCESS_CREATE_ARGS_POINTER` is sufficient.
* - `SDL_PROP_PROCESS_CREATE_SDL_IPC`: true if the process should create an
* IPC channel for sharing resources.
*
* On POSIX platforms, wait() and waitpid(-1, ...) should not be called, and
* SIGCHLD should not be ignored or handled because those would prevent SDL
Expand Down Expand Up @@ -238,6 +245,7 @@ extern SDL_DECLSPEC SDL_Process * SDLCALL SDL_CreateProcessWithProperties(SDL_Pr
#define SDL_PROP_PROCESS_CREATE_STDERR_TO_STDOUT_BOOLEAN "SDL.process.create.stderr_to_stdout"
#define SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN "SDL.process.create.background"
#define SDL_PROP_PROCESS_CREATE_CMDLINE_STRING "SDL.process.create.cmdline"
#define SDL_PROP_PROCESS_CREATE_SDL_IPC "SDL.process.create.ipc"

/**
* Get the properties associated with a process.
Expand All @@ -256,6 +264,8 @@ extern SDL_DECLSPEC SDL_Process * SDLCALL SDL_CreateProcessWithProperties(SDL_Pr
* `SDL_PROP_PROCESS_CREATE_STDERR_NUMBER` set to `SDL_PROCESS_STDIO_APP`.
* - `SDL_PROP_PROCESS_BACKGROUND_BOOLEAN`: true if the process is running in
* the background.
* - `SDL_PROP_PROCESS_SDL_IPC`: true if the process has an IPC channel for
* SDL shared resources.
*
* \param process the process to query.
* \returns a valid property ID on success or 0 on failure; call
Expand All @@ -275,6 +285,7 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetProcessProperties(SDL_Proces
#define SDL_PROP_PROCESS_STDOUT_POINTER "SDL.process.stdout"
#define SDL_PROP_PROCESS_STDERR_POINTER "SDL.process.stderr"
#define SDL_PROP_PROCESS_BACKGROUND_BOOLEAN "SDL.process.background"
#define SDL_PROP_PROCESS_SDL_IPC "SDL.process.ipc"

/**
* Read all the output from a process.
Expand Down Expand Up @@ -432,6 +443,25 @@ extern SDL_DECLSPEC bool SDLCALL SDL_WaitProcess(SDL_Process *process, bool bloc
*/
extern SDL_DECLSPEC void SDLCALL SDL_DestroyProcess(SDL_Process *process);

/**
* Returns a pointer to the process IPC, or NULL if the process has no active
* IPC.
*
* \param process The process to retrieve IPC from.
* \returns A handle to the IPC for the given process, or NULL if the process
* has no active IPC.
*/
extern SDL_DECLSPEC SDL_IPC * SDLCALL SDL_GetProcessIPC(SDL_Process *process);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of these functions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to get the IPC channel as a SDL_IOStream pointer property, like the library currently does for stdio?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface I planned was to have SDL_Shared{Surface,Texture} types that would implement SDL_SendShared{Surface,Texture}(SDL_IPC *) functions. These getters just allow the programmer to get the IPC pointer they want to send to.

This allows us to use the same interface for both SDL_Process objects created with SDL_CreateProcess() and for children that want to send something up to their parent, where there is no obvious SDL_Process object.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels very much like platform specific behavior that's outside the scope of SDL. I'm not opposed to considering it, but you'd need to make a very good use case why this should be in SDL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be out-of-scope, I understand using SDL for multi-process stuff is already probably a niche use-case.

When I was considering writing this for my specific application, I had a rough idea of how to implement it for both Windows and Linux, and intended to also implement it in MacOS in the future, since all these OSes support both shared memory handles and shared GPU memory resources, but they all handle it slightly differently.

Since I was hoping to write a single API that opaquely abstracted away the differences, then attempt to pack the shared memory into SDL_Surface objects and shared GPU memory into SDL_Texture objects, and implement cross-process read/write locking of these resources, to me it made sense to implement this in SDL.

Doing it on the application side is difficult, since instead of just e.g. creating a heap allocation with malloc, on the Posix side we would be using shm_* and mmap to get the pointer, then doing everything that is in the static SDL_InitializeSurface() function. Copy/pasting that function from SDL into the application seems brittle by comparison to me, and while another alternative is to copy from shared memory into a normal SDL_Surface object... I mean, I already have the data in memory, I'd prefer to just wrap it.

I haven't even considered how I would handle SDL_Textures across processes on the application side. From inside the renderer implementation, calling the necessary Mesa functions to get the dma-buf file descriptor and additional data for object construction seems trivial by comparison.

But, I don't want to add more maintenance overhead to the library if the feature doesn't seem that valuable.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, it probably makes sense to flesh it out in your application and then either provide it as an example for others or develop it into a future PR.

I'll go ahead and leave this open and feel free to update it when you're ready.


/**
* Returns a pointer to the SDL IPC for the parent process, or NULL if the
* parent process did not set up SDL IPC.
*
* \returns A handle to the IPC of an SDL-based parent, or NULL if no IPC
* was found.
*/
extern SDL_DECLSPEC SDL_IPC * SDLCALL SDL_GetParentIPC(void);

/* Ends C function definitions when using C++ */
#ifdef __cplusplus
}
Expand Down
2 changes: 2 additions & 0 deletions src/dynapi/SDL_dynapi.sym
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,8 @@ SDL3_0.0.0 {
SDL_RotateSurface;
SDL_LoadSurface_IO;
SDL_LoadSurface;
SDL_GetProcessIPC;
SDL_GetParentIPC;
# extra symbols go here (don't modify this line)
local: *;
};
2 changes: 2 additions & 0 deletions src/dynapi/SDL_dynapi_overrides.h
Original file line number Diff line number Diff line change
Expand Up @@ -1296,3 +1296,5 @@
#define SDL_RotateSurface SDL_RotateSurface_REAL
#define SDL_LoadSurface_IO SDL_LoadSurface_IO_REAL
#define SDL_LoadSurface SDL_LoadSurface_REAL
#define SDL_GetProcessIPC SDL_GetProcessIPC_REAL
#define SDL_GetParentIPC SDL_GetParentIPC_REAL
2 changes: 2 additions & 0 deletions src/dynapi/SDL_dynapi_procs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1304,3 +1304,5 @@ SDL_DYNAPI_PROC(SDL_Cursor*,SDL_CreateAnimatedCursor,(SDL_CursorFrameInfo *a,int
SDL_DYNAPI_PROC(SDL_Surface*,SDL_RotateSurface,(SDL_Surface *a,float b),(a,b),return)
SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadSurface_IO,(SDL_IOStream *a,bool b),(a,b),return)
SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadSurface,(const char *a),(a),return)
SDL_DYNAPI_PROC(SDL_IPC*,SDL_GetProcessIPC,(SDL_Process *a),(a),return)
SDL_DYNAPI_PROC(SDL_IPC*,SDL_GetParentIPC,(void),(),return)
10 changes: 10 additions & 0 deletions src/process/SDL_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,13 @@ void SDL_DestroyProcess(SDL_Process *process)
SDL_DestroyProperties(process->props);
SDL_free(process);
}

SDL_IPC * SDL_GetProcessIPC(SDL_Process *process)
{
return SDL_SYS_GetProcessIPC(process);
}

SDL_IPC * SDL_GetParentIPC(void)
{
return SDL_SYS_GetParentIPC();
}
2 changes: 2 additions & 0 deletions src/process/SDL_sysprocess.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
bool SDL_SYS_KillProcess(SDL_Process *process, bool force);
bool SDL_SYS_WaitProcess(SDL_Process *process, bool block, int *exitcode);
void SDL_SYS_DestroyProcess(SDL_Process *process);
SDL_IPC * SDL_SYS_GetProcessIPC(SDL_Process *process);
SDL_IPC * SDL_SYS_GetParentIPC(void);

#endif // SDL_sysprocess_h_
11 changes: 10 additions & 1 deletion src/process/dummy/SDL_dummyprocess.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

#include "../SDL_sysprocess.h"


bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID props)
{
return SDL_Unsupported();
Expand All @@ -45,4 +44,14 @@ void SDL_SYS_DestroyProcess(SDL_Process *process)
return;
}

SDL_IPC * SDL_SYS_GetProcessIPC(SDL_Process *process)
{
return NULL;
}

SDL_IPC * SDL_SYS_GetParentIPC(void)
{
return NULL;
}

#endif // SDL_PROCESS_DUMMY
126 changes: 122 additions & 4 deletions src/process/posix/SDL_posixprocess.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

#ifdef SDL_PROCESS_POSIX

#define SDLIPC_FILENO 3

#include <dirent.h>
#include <fcntl.h>
#include <errno.h>
Expand All @@ -32,6 +34,8 @@
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <limits.h>

#include "../SDL_sysprocess.h"
#include "../../io/SDL_iostream_c.h"
Expand All @@ -46,8 +50,24 @@
#define READ_END 0
#define WRITE_END 1

#define PARENT_END 0
#define CHILD_END 1

#define SDL_IPC_ENVVAR "_SDL_IPC"

#define _STR(VALUE) #VALUE

// Allows you to stringify the expanded value of a
// macro
#define STR(MACRO) _STR(MACRO)

struct SDL_IPC {
int socket;
};

struct SDL_ProcessData {
pid_t pid;
SDL_IPC ipc;
};

static void CleanupStream(void *userdata, void *value)
Expand Down Expand Up @@ -88,6 +108,19 @@ static void IgnoreSignal(int sig)
}
}

static bool CreateSockets(int fds[2])
{
if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) {
return false;
}

// Make sure the pipe isn't accidentally inherited by another thread creating a process
fcntl(fds[READ_END], F_SETFD, fcntl(fds[READ_END], F_GETFD) | FD_CLOEXEC);
fcntl(fds[WRITE_END], F_SETFD, fcntl(fds[WRITE_END], F_GETFD) | FD_CLOEXEC);

return true;
}

static bool CreatePipe(int fds[2])
{
if (pipe(fds) < 0) {
Expand Down Expand Up @@ -122,14 +155,15 @@ static bool GetStreamFD(SDL_PropertiesID props, const char *property, int *resul
return true;
}

static bool AddFileDescriptorCloseActions(posix_spawn_file_actions_t *fa)
static bool AddFileDescriptorCloseActions(posix_spawn_file_actions_t *fa, bool has_ipc)
{
DIR *dir = opendir("/proc/self/fd");
const int keep_fileno = has_ipc ? SDLIPC_FILENO : STDERR_FILENO;
if (dir) {
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
int fd = SDL_atoi(entry->d_name);
if (fd <= STDERR_FILENO) {
if (fd <= keep_fileno) {
continue;
}

Expand All @@ -144,7 +178,7 @@ static bool AddFileDescriptorCloseActions(posix_spawn_file_actions_t *fa)
}
closedir(dir);
} else {
for (int fd = (int)(sysconf(_SC_OPEN_MAX) - 1); fd > STDERR_FILENO; --fd) {
for (int fd = (int)(sysconf(_SC_OPEN_MAX) - 1); fd > keep_fileno; --fd) {
int flags = fcntl(fd, F_GETFD);
if (flags < 0 || (flags & FD_CLOEXEC)) {
continue;
Expand All @@ -168,11 +202,22 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
SDL_ProcessIO stderr_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_INHERITED);
bool redirect_stderr = SDL_GetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_TO_STDOUT_BOOLEAN, false) &&
!SDL_HasProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER);
bool sdl_ipc = SDL_GetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_SDL_IPC, false);
int stdin_pipe[2] = { -1, -1 };
int stdout_pipe[2] = { -1, -1 };
int stderr_pipe[2] = { -1, -1 };
int sockets[2] = { -1, -1 };
int fd = -1;

if (sdl_ipc) {
// allows children who have already opened a new file descriptor
// to distinguish between "fd 3 came from SDL" vs. "fd 3 is the fd I just opened"
//
// also gives us the option to change fd logic in the future without breaking
// clients
SDL_SetEnvironmentVariable(env, SDL_IPC_ENVVAR, STR(SDLIPC_FILENO), true);
}

// Keep the malloc() before exec() so that an OOM won't run a process at all
envp = SDL_GetEnvironmentVariables(env);
if (!envp) {
Expand Down Expand Up @@ -296,6 +341,16 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
break;
}

if (sdl_ipc) {
if (!CreateSockets(sockets)) {
goto posix_spawn_fail_all;
}
if (posix_spawn_file_actions_adddup2(&fa, sockets[CHILD_END], SDLIPC_FILENO) != 0) {
SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
goto posix_spawn_fail_all;
}
}

if (redirect_stderr) {
if (posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, STDERR_FILENO) != 0) {
SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s", strerror(errno));
Expand Down Expand Up @@ -333,7 +388,7 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
}
}

if (!AddFileDescriptorCloseActions(&fa)) {
if (!AddFileDescriptorCloseActions(&fa, sdl_ipc)) {
goto posix_spawn_fail_all;
}

Expand Down Expand Up @@ -400,6 +455,12 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
close(stderr_pipe[WRITE_END]);
}

if (sdl_ipc) {
data->ipc.socket = sockets[PARENT_END];
close(sockets[CHILD_END]);
}
SDL_SetBooleanProperty(process->props, SDL_PROP_PROCESS_SDL_IPC, sdl_ipc);

posix_spawn_file_actions_destroy(&fa);
posix_spawnattr_destroy(&attr);
SDL_free(envp);
Expand Down Expand Up @@ -433,6 +494,12 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
if (stderr_pipe[WRITE_END] >= 0) {
close(stderr_pipe[WRITE_END]);
}
if (sockets[PARENT_END] >= 0) {
close(sockets[PARENT_END]);
}
if (sockets[CHILD_END] >= 0) {
close(sockets[CHILD_END]);
}
SDL_free(envp);
return false;
}
Expand Down Expand Up @@ -511,4 +578,55 @@ void SDL_SYS_DestroyProcess(SDL_Process *process)
SDL_free(process->internal);
}

SDL_IPC * SDL_SYS_GetProcessIPC(SDL_Process *process)
{
SDL_ProcessData *data = (SDL_ProcessData*)process->internal;
return &data->ipc;
}

SDL_IPC * SDL_SYS_GetParentIPC(void)
{
const char *SDL_GetEnvironmentVariable(SDL_Environment *env, const char *name);

// hmmmm
static SDL_IPC ipc = {
.socket = -1,
};

SDL_Environment *env = NULL;

if (ipc.socket == -1) {
env = SDL_GetEnvironment();
if (!env) {
goto return_null;
}

const char *env_fd = SDL_GetEnvironmentVariable(env, SDL_IPC_ENVVAR);
if (!env_fd) {
goto destroy_environment;
}

char *end = NULL;
long result = strtol(env_fd, &end, 10);

const bool error = end == NULL
|| end == env_fd
|| result > INT_MAX
|| result < 0;

if (error) {
goto destroy_environment;
}

ipc.socket = (int)result;
}

return &ipc;

destroy_environment:
SDL_DestroyEnvironment(env);
return_null:
return NULL;
}

#endif // SDL_PROCESS_POSIX
Loading
Loading