Alas, your Linux requirement rules out
NSXPCConnection, which is my preferred mechanism for this sort of thing. This means you really are stuck with UNIX-y primitives, processes, pipes, and so on. Those primitives are… well… primitive, that is, they are the place to start for problems like this but you’ll need to layer a bunch of extra stuff on top. For example, when you send a message to another process over a pipe, you are responsible for serialising that message to a byte stream. There are lots of ways to do this — and some of them, like
Codable and protobuf, are quite Swift friendly — but it’s still your responsibility.
(1) you can't pass e.g. a closure for execution in a separate process
This is not feasible in any multi-process system. If that’s an absolute requirement, the only way forward is to run your core emulator code in process, changing it to not rely on global variables.
(2) there is no simple way to share memory between the processes so I
don't have to keep copying data around.
It’s certainly possible to share memory between processes (for example, using
shm_open man page). However, I think it’s fair to say that this is not “simple”.
Apropos UNIX-y primitives, one word of warning. jonprescott wrote:
However, as I have done many times in the past on a variety of Unix
variants, you can use fork only, which will create a new process which
is an exact copy of the calling process.
If you’re creating a library, and thus have no control over the type of program that your library is loaded in, this isn’t viable given your platform requirements. That’s because, on Apple platforms, doing a
fork without an
exec is only safe if you stick with the very lowest level frameworks. High-level frameworks lean heavily into Mach, and run into problems because the Mach port namespace  is not copied to the child process.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
 Apart from the special ports.