projects/2023–26

Objective-C on the Sega Dreamcast

Getting the Objective-C runtime and GNUstep Foundation running on a 1998 console.

  • Objective-C
  • C
  • KallistiOS
  • GNUstep
  • SH-4
  • Make
Objective-C code that counts the letters in 'sega dreamcast' with NSMutableDictionary

Objective-C is mostly thought of as Apple's language, but the runtime is just C. That makes it portable to anything with a C toolchain, including the Hitachi SH-4 inside a Sega Dreamcast. This project added Objective-C support to KallistiOS, the open-source Dreamcast SDK: the libobjc runtime, then GNUstep's libs-base (Foundation), with example programs that double as toolchain sanity checks.

What it took

Getting #import <Foundation/Foundation.h> to compile for a console with 16 MB of RAM meant working through the whole stack:

  • Runtime: building GCC’s libobjc for the sh-elf target and linking it through KallistiOS’s toolchain wrappers.
  • libffi: NSInvocation and message forwarding need a foreign-function interface, so libffi had to build for the SH-4.
  • GNUstep make + libs-base: porting the build system and Foundation itself to a target with no dynamic loader, a minimal libc and no real filesystem.

The runtime example

The first example tests the bare runtime: no Foundation, just Object, instance variables, properties and message sends, compiled with kos-cc and running on real hardware. Falco Girgis and I wrote it together.

@interface Person: Object
{
    const char *_name;
    int _age;
    float _height;
    Person *_bestFriend;
    BOOL _dead;
}
- (void)addName:(const char *)name age:(int)age height:(float)height;
- (void)setBestFriend:(Person *)bestFriend;
@property(nonatomic, assign)Person *bestFriend;
@property(nonatomic)BOOL dead;
@end

examples/dreamcast/objc/runtime/runtime.m

It then uses the runtime’s C API to create, inspect and copy objects:

// Create a new instance of person, using the runtime
person1 = class_createInstance(objc_getClass("Person"), 0);

// Call message handler for "addName," setting instance variables
[person1 addName: "Joe" age: 20 height: 6.1f];

// Use runtime to copy construct another Person instance
person2 = object_copy(person1, 0);

Building it is a normal KallistiOS Makefile that links -lobjc:

$(TARGET): $(OBJS)
	kos-cc -o $(TARGET) $(OBJS) -lobjc

Foundation, with a homemade test runner

The second example brings in GNUstep Foundation. There’s no XCTest on a Dreamcast, so I wrote a small runner that finds test methods the same way XCTest does, by reflecting over each class and calling every selector whose name contains test:

Method *methodList = class_copyMethodList(testClass, &outCount);
id tests = class_createInstance(testClass, 0);
for(int ii = outCount-1; ii >= 0; ii--) {
    SEL selector = method_getName(methodList[ii]);
    const char *selectorName = sel_getName(selector);
    if (strstr(selectorName, "test")) {
        IMP imp = objc_msg_lookup(tests, selector);
        id testCaseResult = imp(tests, selector);
        ...
    }
}

examples/dreamcast/objc/foundation/foundation.m

The container tests exercise NSArray, NSDictionary and NSSet. My favourite counts the letters in the console’s own name:

- (int) testNSDictionary {
    NSMutableDictionary *container = [NSMutableDictionary dictionary];

    NSString *consoleName = [@"sega dreamcast" stringByReplacingOccurrencesOfString: @" " withString: @""];
    for (NSInteger i = 0; i < consoleName.length; i++) {
        unichar character = [consoleName characterAtIndex:i];
        NSString *letter = [NSString stringWithCharacters:&character length:1];
        NSNumber *letterCount = [container objectForKey:letter];
        if (!letterCount) {
            [container setObject:[NSNumber numberWithInteger: 1] forKey:letter];
        } else {
            [container setObject:[NSNumber numberWithInt:[letterCount intValue] + 1] forKey:letter];
        }
    }
    ...
}

examples/dreamcast/objc/foundation/Containers/ContainerTests.m