Introduction to macOS Distributed Objects
Distributed Objects (DO) is one of the oldest and most elegant inter-process communication (IPC) mechanisms on Apple platforms. Originally introduced in NeXTSTEP and carried forward into macOS, it allows one process to send Objective-C messages to objects living in a completely different process — potentially on a different machine across the network. While Apple has since deprecated DO in favor of XPC and modern RPC frameworks, it remains a fascinating piece of Cocoa history and a useful tool for understanding how high-level IPC abstractions are built.
This tutorial walks through what Distributed Objects is, why it matters, how to build a working client and server, and the best practices you should follow if you ever need to maintain or reason about legacy DO-based code.
What Is Distributed Objects?
Distributed Objects is an Objective-C runtime feature that transparently extends message passing across process boundaries. When you call a method on a "remote" object, the call is serialized into an NSInvocation, transported over an NSPort (typically a Mach port locally or a socket port over the network), and re-executed in the host process. Return values and byref arguments are shipped back the same way.
The core classes involved are:
NSConnection— establishes and manages the link between two processes.NSPort— the underlying transport (Mach port, socket port, or message port).NSPortNameServer— registers and looks up named connections so clients can find servers.NSDistantObject— the local proxy that forwards messages to the remote object.
From the developer's perspective, calling a remote object looks almost identical to calling a local one. The runtime handles marshaling, transport, and dispatch. This transparency is both DO's greatest strength and its biggest weakness, as we will see.
Why It Matters
Even though Distributed Objects is deprecated, understanding it matters for several reasons:
- Legacy maintenance — many older macOS apps and system frameworks still rely on DO internally.
- Conceptual foundation — DO is a textbook example of a transparent RPC proxy, and the same pattern appears in modern systems (gRPC stubs, Java RMI, .NET Remoting).
- Security lessons — DO's failures (no sandboxing, no type verification at the wire level, easy to crash a server with malformed invocations) directly motivated the design of XPC.
- Quick prototyping — for non-production tooling, DO is still one of the fastest ways to expose an Objective-C object to another process.
How to Use Distributed Objects
1. Define a Protocol
The first step is to define the interface the server will expose. Always use a formal @protocol so both sides agree on method signatures. Without a protocol, the proxy falls back to NSObject's signature, which can cause subtle marshaling bugs.
#import <Foundation/Foundation.h>
@protocol CalculatorService <NSObject>
- (NSInteger)add:(NSInteger)a to:(NSInteger)b;
- (NSString *)greetingForName:(NSString *)name;
- (NSDictionary *)statistics;
@end
2. Implement the Server
The server creates an instance of the implementing class, registers it with an NSConnection, and publishes the connection under a name through NSConnection's default name server. The run loop must keep spinning so incoming port messages can be dispatched.
#import <Foundation/Foundation.h>
#import "CalculatorService.h"
@interface Calculator : NSObject <CalculatorService>
@end
@implementation Calculator
- (NSInteger)add:(NSInteger)a to:(NSInteger)b {
NSLog(@"Server: computing %ld + %ld", (long)a, (long)b);
return a + b;
}
- (NSString *)greetingForName:(NSString *)name {
return [NSString stringWithFormat:@"Hello, %@!", name];
}
- (NSDictionary *)statistics {
return @{@"uptime": @(1234), @"requests": @(42)};
}
@end
int main(int argc, const char *argv[]) {
@autoreleasepool {
Calculator *calc = [[Calculator alloc] init];
NSConnection *connection =
[NSConnection connectionWithReceivePort:[NSPort port]
sendPort:nil];
// Export the object under a name clients can look up.
[connection setRootObject:calc];
[connection registerName:@"com.example.Calculator"];
NSLog(@"Calculator server registered and running...");
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
Key points about the server:
connectionWithReceivePort:sendPort:creates a connection that only receives. The send port isnilfor a pure server.setRootObject:marks the object that clients will receive when they connect.registerName:publishes the connection through the system's defaultNSPortNameServer, which on macOS uses Mach bootstrap names.- The run loop is mandatory — DO is entirely asynchronous underneath and depends on the run loop to drain port messages.
3. Implement the Client
The client connects to the registered name, retrieves the root object as a proxy, and calls methods on it as if it were local.
#import <Foundation/Foundation.h>
#import "CalculatorService.h"
int main(int argc, const char *argv[]) {
@autoreleasepool {
NSConnection *connection =
[NSConnection connectionWithRegisteredName:@"com.example.Calculator"
host:nil];
if (!connection) {
NSLog(@"Failed to connect to server");
return 1;
}
id <CalculatorService> proxy =
(id <CalculatorService>)[connection rootObject];
// Calls are transparently forwarded across the process boundary.
NSInteger sum = [proxy add:7 to:35];
NSLog(@"7 + 35 = %ld", (long)sum);
NSString *greeting = [proxy greetingForName:@"Developer"];
NSLog(@"%@", greeting);
NSDictionary *stats = [proxy statistics];
NSLog(@"Stats: %@", stats);
}
return 0;
}
Passing nil for the host: argument means "look up the name on the local machine." To reach a server on another host, pass a hostname string — DO will use socket-based transport automatically. Note that remote DO over TCP is even more deprecated than local DO and is blocked by default on modern macOS.
4. Handling Errors and Timeouts
Because the proxy is an NSDistantObject, you can configure timeouts and behavior on it. By default, a remote call blocks indefinitely if the server hangs. You should always set a request timeout for production code.
NSDistantObject *distant = (NSDistantObject *)proxy;
[distant setRequestTimeout:5.0]; // seconds to wait for a reply
[distant setReplyTimeout:5.0]; // seconds to wait for the server to start replying
@try {
NSInteger result = [proxy add:100 to:200];
NSLog(@"Result: %ld", (long)result);
} @catch (NSException *e) {
NSLog(@"Remote call failed: %@ — %@", e.name, e.reason);
}
Remote exceptions are re-raised in the caller's context as NSPortTimeoutException, NSInvalidSendPortException, or NSConnectionTimeoutException, among others. Always wrap remote calls in @try/@catch.
5. Passing Objects by Reference
By default, object arguments are passed by copy (archived with NSCoder) and scalar arguments by value. To pass an object by reference so the server can mutate it, mark the parameter with byref in the protocol:
@protocol MutatorService <NSObject>
- (void)fillArray:(NSMutableArray *)array byref;
@end
When the parameter is marked byref, DO ships a proxy for the object instead of a copy, and mutations made on the server side are visible to the caller. Use this sparingly — it introduces shared mutable state across a process boundary, which is a classic source of deadlocks and races.
Best Practices
- Always declare a formal protocol. Without it, the proxy cannot verify argument types and may silently corrupt data during marshaling.
- Keep interfaces small and coarse-grained. Each remote call crosses a process boundary and is far more expensive than a local message send. Batch related work into a single method rather than chattering across the wire.
- Prefer value types and immutable objects over mutable ones. Immutable
NSString,NSNumber,NSArray, andNSDictionaryarchive cleanly and avoid the pitfalls ofbyrefproxies. - Set explicit timeouts. A hung server should not hang your client forever. Configure both
requestTimeoutandreplyTimeout. - Wrap every remote call in exception handling. Network failures, server crashes, and serialization errors all surface as exceptions at the call site.
- Do not expose privileged objects. DO has no authentication or authorization model. Any process that can look up the registered name can invoke any method on the root object. Treat the exported surface as fully public.
- Avoid DO for new code. Use XPC for sandboxed IPC, or higher-level frameworks like SwiftUI app extensions, URL schemes, or HTTP-based services for cross-machine communication. DO is appropriate only for legacy maintenance or quick internal tooling.
- Run the server on a dedicated thread or dispatch queue. Incoming invocations are dispatched on the run loop's thread, so long-running server methods will block other clients. Consider routing work to a background queue from inside each method.
- Validate inputs server-side. Because the protocol is not enforced at the wire level, a malicious or buggy client can pass unexpected types. Check arguments before acting on them.
Conclusion
Distributed Objects is a remarkably transparent IPC abstraction that lets one process invoke Objective-C methods on objects in another process with almost no ceremony. By combining NSConnection, a registered name, and a formal protocol, you can stand up a working client and server in a few dozen lines of code. However, that transparency comes at a cost: no sandboxing, no built-in security, fragile type handling, and a tendency to hide just how expensive each remote call really is. For new macOS software, XPC is the right tool for the job — but understanding how DO works gives you a clearer mental model of RPC design in general and is essential for anyone maintaining legacy Cocoa codebases. Use it judiciously, respect its limitations, and you will find it a useful, if dated, entry in the macOS IPC toolbox.