objc-js allows you to define new Objective-C classes and subclass existing ones from JavaScript. This enables you to override methods, implement custom behavior, and integrate deeply with macOS frameworks.
- Basic Usage
- Defining a Class
- Method Type Encodings
- Calling Super
- Protocol Conformance
- Complete Examples
- Limitations
Use NobjcClass.define() to create a new Objective-C class:
import { NobjcClass } from "objc-js";
const MyClass = NobjcClass.define({
name: "MyCustomClass",
superclass: "NSObject",
methods: {
customMethod: {
types: "v@:", // void return, self, _cmd
implementation: (self) => {
console.log("Custom method called!");
}
}
}
});
// Create an instance
const instance = MyClass.alloc().init();
instance.customMethod();The NobjcClass.define() method takes a ClassDefinition object with the following properties:
{
name: "MyClassName"; // Must be unique across your app
}The class name must be unique. Attempting to define a class with an existing name will throw an error.
{
superclass: "NSObject"; // Any Objective-C class name
}You can subclass any Objective-C class:
"NSObject"- Base class for most objects"NSView"- macOS views"NSViewController"- macOS view controllers- Any other framework class
Note: Subclassing class clusters (like NSString, NSArray, NSDictionary) requires implementing primitive methods and is not currently supported.
{
methods: {
"methodName": {
types: "@@:", // Type encoding
implementation: (self, ...args) => {
// Method implementation
}
},
"methodWith$arg$": {
types: "v@:@i", // void return, object arg, int arg
implementation: (self, obj, num) => {
console.log(obj, num);
}
}
}
}- Use the Objective-C selector directly for simple methods:
"init","description" - Use
$notation for methods with arguments:"methodWith$arg$"formethodWith:arg:
The implementation function receives:
self- The instance receiving the message (as aNobjcObject)- Remaining arguments as specified in the method signature
The function should return the value specified by the return type encoding.
{
protocols: ["NSCopying", "NSCoding"];
}Specify protocol names the class should conform to. The class will be registered as conforming to these protocols, and you must implement the required methods.
Type encodings describe the method signature using Objective-C type encoding notation.
[return type][self][@][_cmd][:][argument types...]
[return type]- Single character for return type@- Type forself:- Type for_cmd(selector)[argument types]- Types for each argument (beyondselfand_cmd)
| Code | Type | JavaScript Type |
|---|---|---|
v |
void | undefined |
@ |
id (object) | NobjcObject |
# |
Class | NobjcObject (class) |
c |
char | number |
i |
int | number |
s |
short | number |
l |
long | number |
q |
long long (NSInteger) | number |
C |
unsigned char | number |
I |
unsigned int | number |
S |
unsigned short | number |
L |
unsigned long | number |
Q |
unsigned long long (NSUInteger) | number |
f |
float | number |
d |
double | number |
B |
BOOL | boolean |
* |
char* (C string) | string |
^v |
void* (pointer) | (varies) |
// void method, no arguments
"v@:";
// Returns NSString*, no arguments
"@@:";
// Returns NSInteger, no arguments
"q@:";
// Returns BOOL, no arguments
"B@:";
// void method, takes NSString* argument
"v@:@";
// Returns NSString*, takes NSString* and NSInteger arguments
"@@:@q";
// Returns id, takes id and BOOL arguments
"@@:@B";
// Returns void, takes pointer argument (e.g., NSZone*)
"v@:^v";Use NobjcClass.super() to call the superclass implementation:
const MyClass = NobjcClass.define({
name: "MyClass",
superclass: "NSObject",
methods: {
description: {
types: "@@:",
implementation: (self) => {
// Call super's description
const superDesc = NobjcClass.super(self, "description");
// Modify and return
const prefix = NSString.stringWithUTF8String$("Custom: ");
return prefix.stringByAppendingString$(superDesc);
}
}
}
});NobjcClass.super(self, selectorName, ...args);self- The instance (received as first argument in method implementation)selectorName- The Objective-C selector name (e.g.,"description","initWithString:","contentsOfDirectoryAtPath:error:")...args- Arguments to pass to the superclass method
// Calling super.init()
init: {
types: "@@:",
implementation: (self) => {
self = NobjcClass.super(self, "init");
// Custom initialization
return self;
}
}// Calling super with an object argument
isEqual$: {
types: "B@:@",
implementation: (self, other) => {
// First check if super considers them equal
const superEqual = NobjcClass.super(self, "isEqual:", other);
if (superEqual) return true;
// Custom equality check
return myCustomEqualityCheck(self, other);
}
}Methods with out-parameters (like NSError**) are fully supported:
// Calling super with an NSError** out-parameter
contentsOfDirectoryAtPath$error$: {
types: "@@:@^@", // Returns NSArray*, takes NSString* and NSError**
implementation: (self, path, errorPtr) => {
console.log("Custom directory listing for:", path.toString());
// Call super - pass null for errorPtr
const contents = NobjcClass.super(
self,
"contentsOfDirectoryAtPath:error:",
path,
null // or errorPtr if you want to handle errors
);
// Custom processing
if (contents) {
console.log("Found", contents.count(), "items");
}
return contents;
}
}Note: For out-parameters like NSError**, you can pass null from JavaScript. The native code will allocate the appropriate storage and pass a valid pointer to the superclass method.
To conform to a protocol, specify it in the protocols array and implement its required methods:
const MyClass = NobjcClass.define({
name: "MyCopyable",
superclass: "NSObject",
protocols: ["NSCopying"],
methods: {
"copyWithZone:": {
types: "@@:^v", // Returns id, takes NSZone* pointer
implementation: (self, zone) => {
// Create a copy
const copy = MyClass.alloc().init();
// ... copy data ...
return copy;
}
}
}
});import { NobjcLibrary, NobjcClass } from "objc-js";
const foundation = new NobjcLibrary("/System/Library/Frameworks/Foundation.framework/Foundation");
const NSString = foundation["NSString"];
const MyClass = NobjcClass.define({
name: "SimpleSubclass",
superclass: "NSObject",
methods: {
greeting: {
types: "@@:",
implementation: (self) => {
return NSString.stringWithUTF8String$("Hello from my subclass!");
}
}
}
});
const instance = MyClass.alloc().init();
const message = instance.greeting();
console.log(message.toString()); // "Hello from my subclass!"const MyClass = NobjcClass.define({
name: "InitExample",
superclass: "NSObject",
methods: {
init: {
types: "@@:",
implementation: (self) => {
// Always call super.init() and return its result
self = NobjcClass.super(self, "init");
// Do custom initialization here
console.log("Custom initialization!");
return self;
}
}
}
});
const instance = MyClass.alloc().init(); // Logs "Custom initialization!"const MyClass = NobjcClass.define({
name: "ArgumentExample",
superclass: "NSObject",
methods: {
processString$withNumber$: {
types: "v@:@q", // void, NSString*, NSInteger
implementation: (self, str, num) => {
console.log("String:", str.toString());
console.log("Number:", num);
}
},
addNumbers$and$: {
types: "q@:qq", // returns NSInteger, takes two NSIntegers
implementation: (self, a, b) => {
return a + b;
}
}
}
});
const instance = MyClass.alloc().init();
const str = NSString.stringWithUTF8String$("Test");
instance.processString$withNumber$(str, 42);
const sum = instance.addNumbers$and$(10, 20);
console.log(sum); // 30const MyClass = NobjcClass.define({
name: "DescriptionExample",
superclass: "NSObject",
methods: {
description: {
types: "@@:",
implementation: (self) => {
const superDesc = NobjcClass.super(self, "description");
const prefix = NSString.stringWithUTF8String$("MyClass(");
const suffix = NSString.stringWithUTF8String$(")");
return prefix.stringByAppendingString$(superDesc).stringByAppendingString$(suffix);
}
}
}
});
const instance = MyClass.alloc().init();
console.log(instance.description().toString());
// "MyClass(<DescriptionExample: 0x123456789>)"This example shows how to subclass ASAuthorizationController to override a private method for WebAuthn/passkey operations. This demonstrates calling super with both object arguments and out-parameters:
import { NobjcLibrary, NobjcClass } from "objc-js";
const authServices = new NobjcLibrary(
"/System/Library/Frameworks/AuthenticationServices.framework/AuthenticationServices"
);
const foundation = new NobjcLibrary("/System/Library/Frameworks/Foundation.framework/Foundation");
const NSString = foundation["NSString"];
const NSData = foundation["NSData"];
const ASAuthorizationController = authServices["ASAuthorizationController"];
// Your clientDataHash for WebAuthn
const clientDataHash = Buffer.from("your-hash-here", "base64");
const MyAuthController = NobjcClass.define({
name: "MyASAuthorizationController",
superclass: "ASAuthorizationController",
methods: {
"_requestContextWithRequests:error:": {
types: "@@:@^@", // Returns id, takes NSArray*, NSError**
implementation: (self, requests, errorPtr) => {
console.log("Custom implementation called");
// Call super with arguments including the error out-parameter
// Pass null for errorPtr - the native code handles this correctly
const context = NobjcClass.super(
self,
"_requestContextWithRequests:error:",
requests,
null // errorPtr - automatically handled
);
if (context) {
// Create NSData from Buffer
const hashData = NSData.alloc().initWithBytes$length$(clientDataHash, clientDataHash.length);
// Set the clientDataHash on the context
context.setClientDataHash$(hashData);
console.log("clientDataHash set successfully");
}
return context;
}
}
}
});
// Use your custom controller instead of ASAuthorizationController
const controller = MyAuthController.alloc().initWithAuthorizationRequests$(requests);
controller.setDelegate$(delegate);
controller.performRequests();Key Points:
- The method signature includes
^@for theNSError**out-parameter - When calling super, you can pass
nullfor out-parameters - The native code properly allocates storage and passes valid pointers to the superclass
- This pattern works for any method with out-parameters (NSError*, id, etc.)
This example demonstrates overriding a method with multiple arguments, including an out-parameter:
import { NobjcLibrary, NobjcClass } from "objc-js";
const foundation = new NobjcLibrary("/System/Library/Frameworks/Foundation.framework/Foundation");
const NSFileManager = foundation["NSFileManager"];
const NSString = foundation["NSString"];
const LoggingFileManager = NobjcClass.define({
name: "LoggingFileManager",
superclass: "NSFileManager",
methods: {
contentsOfDirectoryAtPath$error$: {
types: "@@:@^@", // Returns NSArray*, takes NSString* path, NSError** error
implementation: (self, path, errorPtr) => {
console.log("Reading directory:", path.toString());
// Call super with both arguments
const contents = NobjcClass.super(
self,
"contentsOfDirectoryAtPath:error:",
path,
null // errorPtr handled automatically
);
if (contents) {
const count = contents.count();
console.log(` Found ${count} items`);
} else {
console.log(" Failed to read directory");
}
return contents;
}
}
}
});
const manager = LoggingFileManager.alloc().init();
const tmpPath = NSString.stringWithUTF8String$("/tmp");
const contents = manager.contentsOfDirectoryAtPath$error$(tmpPath, null);-
Class clusters: Cannot subclass class clusters like
NSString,NSArray,NSDictionary,NSNumber, orNSMutableStringbecause they require implementing primitive methods. -
Struct return types: Methods that return structs (like
NSRect,NSPoint,NSSize) may not work correctly on all architectures. -
Variadic methods: Cannot define or call methods with variadic arguments (e.g.,
stringWithFormat:, ...).
Out-parameters (like NSError**, id**) are fully supported in both method definitions and super calls:
- When defining methods with out-parameters, declare them in the type encoding (e.g.,
^@forNSError**) - When calling super with out-parameters, pass
nullfrom JavaScript - The native code automatically allocates the required storage and passes valid pointers
- If the superclass writes an error, it will be stored but not currently accessible from JavaScript (this is a limitation for future enhancement)
- Method implementations are called on the same thread that invokes the Objective-C method
- If the Objective-C runtime calls your method from a background thread, your JavaScript code will be marshaled to the JavaScript thread
- Return values are properly synchronized across threads
- Subclass definitions are kept alive for the lifetime of the application
- JavaScript method implementations are automatically retained
- Instance data should be managed carefully - consider using associated objects or weak maps if you need per-instance state