mach_port_t for inter-process communication

Mach ports are a way for processes to communicate in Mac OS X. (Other mechanisms for inter-process communication include distributed objects and sockets.) A mach port is an endpoint of a communication channel. If 2 processes hold endpoints to the same communication channel, then one process can send messages to the other.

Mac OS X provides wrappers around mach ports — NSMachPort and CFMachPort. But sometimes you have to drop down to the native mach_port_t API.

Don’t do this on whim. I had to drop down to mach_port_t because NSMachPort and CFMachPort both require runloops, which don’t always work for what I was writing — a Quartz Composer plug-in. The problem is the steep learning curve — obscure API, lack of understandable documentation, lack of tutorials and examples. I hope this article makes it a little easier for the next person.

Basic concepts

Mach 3 Kernel Principles gives an overview of mach port concepts — tasks, messages, rights, etc. It’s a good overview, if you can tolerate ambiguity (“A port is named by port rights…” “Port names that name receive rights…” “Ports, themselves, are not named…”). MACH Kernel Interface Manual doesn’t explain as much but seems a little more precise with terms.

“Supposedly a writer isn’t supposed to ‘dumb down’ the material, but that’s the only way I can get it to the point where I can understand it.” — Joel Achenbach

You can skip the manuals for now. Here’s what you need to know:

  • A task in Mac OS X is a process.
  • A task can hold a reference to a port. This reference (is it a port name? a port right name? unclear) has type mach_port_t, which is just an unsigned integer.
  • A port can be on either the sending end or the receiving end of a communication channel.
  • The communication channel consists of a queue for messages.
  • A message is a kind of data structure. It contains, among other information, the message body (e.g., an unsigned integer to be sent from one process to another) and a reference to the port through which it will be sent.
  • A port has a setting, or set of settings, called its rights. There are receive rights, send rights, and send-once rights. A port needs to have the correct rights to be able to send or receive messages.
  • Associated with each task are a few special built-in ports. One of them is the bootstrap port, which other processes can send messages to if they know about it. When a process forks a child, the child inherits the bootstrap port of the parent.

Establishing communication

Check out this example from Random Bits and Pieces. The sampling_fork function shows how to establish 2-way communication between a parent process and a child process. The steps are:

  1. Parent creates a port that has the ability to receive messages (parent_recv_port). Parent temporarily replaces its bootstrap port with this port.
  • Edit (2011.08.28): Replacing the bootstrap port does not work in Lion (the child process hangs), and, we’re told by Apple Developer Technical Support, was never guaranteed to work. The problem in Lion is that XPC tries to message the bootstrap port after it’s been changed, and when that fails, things break. Instead, you can have the parent register itself by name (a string) with the bootstrap server using bootstrap_register, and have the child look up the parent using bootstrap_look_up. These are declared in <servers/bootstrap.h>. For an example of setting up the port to pass to bootstrap_register, see the register_service function here. (You don’t have to do any corresponding setup on the port passed to bootstrap_look_up.) Caveats: bootstrap_register is deprecated, and it’s insecure since it would allow other processes launched by the same user to communicate with it.
  1. Parent forks child. Child inherits bootstrap port from parent.
  2. Parent restores its original bootstrap port. This is cleanup.
  3. Child copies its bootstrap port — the parent_recv_port that parent sent — into its own copy of parent_recv_port. Now parent and child hold endpoints to the same communication channel, and child can send to parent.
  4. (This step is not actually needed to establish communication through ports. It’s needed in this example because the parent needs a reference to the child’s task so it can call task_info.) Child sends a reference to its task (mach_task_self()) to parent through child’s parent_recv_port. Parent receives the reference to the child’s task through parent’s parent_recv_port and copies it to child_task.
  5. Child creates a port that has the ability to receive messages (child_recv_port) and sends it to parent through child’s parent_recv_port. Parent receives the port through parent’s parent_recv_port and copies it to parent’s child_recv_port.
  6. The rest is cleanup. Parent sends its original bootstrap port to child through parent’s child_recv_port. Child receives the port through child’s child_recv_port and uses it to restore its bootstrap port.

In step 2, the parent can’t just pass the integer representing parent_recv_port to the child as a command-line argument because each task has its own names for ports. If the parent and the child each print the value of parent_recv_port, they will most likely be different integers.

I hope you enjoyed that example of establishing communication with mach_port_t. It’s the only one in the world.

(There is an example in The Unofficial GNU Mach IPC Beginner’s Guide. But it doesn’t compile for me, and anyway it’s GPL so incompatible with proprietary software.)

Passing messages

Once the ports are set up, you can use them to send and receive data. You can send unsigned integers, or it’s rumored that you can send “out-of-line” data (memory references). Here are functions to send and receive an unsigned integer, modeled after send_port and recv_port:

int send_uint(mach_port_t remote_port, unsigned int msg_data)
{
    struct {
        mach_msg_header_t header;
        mach_msg_body_t body;
        mach_msg_type_descriptor_t type;
    } msg;
 
    msg.header.msgh_remote_port = remote_port;
    msg.header.msgh_local_port = MACH_PORT_NULL;
    msg.header.msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, 0);
    msg.header.msgh_size = sizeof msg;
 
    msg.body.msgh_descriptor_count = 1;
    msg.type.pad1 = msg_data; 
    msg.type.pad2 = sizeof(msg_data); 
 
    mach_msg_return_t err = mach_msg_send(&msg.header); 
    if (err != MACH_MSG_SUCCESS)
    {
        fprintf(stderr, "Couldn't send uint: 0x%x\n", err); 
        return -1; 
    }
 
    return 0;
}
int recv_uint(mach_port_t recv_port, unsigned int *msg_data) 
{
    struct {
        mach_msg_header_t header;
        mach_msg_body_t body; 
        mach_msg_type_descriptor_t type; 
        mach_msg_trailer_t trailer;
    } msg;
 
    mach_msg_return_t err = mach_msg (&msg.header, MACH_RCV_MSG | MACH_RCV_INTERRUPT,
                                      0, sizeof msg, recv_port,
                                      MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); 
    if (err != MACH_MSG_SUCCESS)
    {
        fprintf(stderr, "Couldn't receive uint: 0x%X\n", err); 
        return -1; 
    }
 
    *msg_data = msg.type.pad1; 
    return 0;
}

The call to mach_msg in recv_uint is interruptable. You can also make it timeoutable by adding the MACH_RCV_TIMEOUT flag to the 2nd argument and setting a timeout in the 6th argument.

Cleaning up

You have to clean up after mach ports, since there are a finite number of them and unused ports eventually need to be recycled. Mach uses reference counting — it counts references to port rights. The general way to decrement reference counts is with mach_port_mod_refs. Or, for a send right, you can decrement the reference count with the easier function mach_port_deallocate. When the port gets down to a reference count of 0 for all of its different kinds of rights (send, receive, etc.), the port is destroyed by the kernel. Though the API documentation recommends against it, you can force a port to be destroyed with mach_port_destroy.

You can check the reference counts on a port right with mach_port_get_refs. Apple provides a MachPortDump tool that prints information, including port rights, for every port that your task knows about. One way to check for mach port leaks is to print each mach_port_t value when it’s created, then call PrintProcessPortSpace after the port is supposed to have been destroyed; if it’s still in the list, it hasn’t been destroyed. (If it’s listed as a dead name, it can’t send or receive messages anymore but it won’t be recycled either.)

The Random Bits and Pieces example has an example of mach_port_deallocate — but it’s broken. Based on the output of MachPortDump, none of the ports in this example are getting destroyed. Here are the reference counts for rights on each port just after they are set up or received:

Process Port Send rights Receive rights
Parent parent_recv_port 1 1
Parent child_recv_port 1 0
Child parent_recv_port 2 0
Child child_recv_port 1 1

The following code destroys all ports. At the end of the child case, add:

mach_port_mod_refs(mach_task_self(), child_recv_port, MACH_PORT_RIGHT_RECEIVE, -1); 
mach_port_deallocate(mach_task_self(), child_recv_port); 
mach_port_mod_refs(mach_task_self(), child_recv_port, MACH_PORT_RIGHT_SEND, -2); 

At the end of the parent case, add:

mach_port_mod_refs(mach_task_self(), parent_recv_port, MACH_PORT_RIGHT_RECEIVE, -1); 
mach_port_deallocate(mach_task_self(), parent_recv_port); 
mach_port_deallocate(mach_task_self(), child_recv_port); 

API reference

API docs are available as a PDF or in HTML but missing some functions (at least mach_msg_send).


Jaymie Strecker is a software developer at Kosada, Inc. and one of the creators of Vuo.