← Back to DevBytes

macOS Distributed Objects

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:

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:

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles