Skip to content

Instantly share code, notes, and snippets.

@jamonholmgren
Last active August 6, 2025 13:45
Show Gist options
  • Save jamonholmgren/e0f3bb8b0a71b8b97c64cec7397745b5 to your computer and use it in GitHub Desktop.
Save jamonholmgren/e0f3bb8b0a71b8b97c64cec7397745b5 to your computer and use it in GitHub Desktop.
Probably one of my more cursed spelunking expeditions. Run Objective-C from a string passed in from JavaScript.

Running Objective-C (sorta) from JavaScript directly

To do this:

const result = invokeObjC(`[[NSString stringWithString:[[[@[@"One", @"Two", @"Three"] mutableCopy] addObject:@"Four"] componentsJoinedByString:@" | "]] uppercaseString]`)
console.log(result) // "ONE | TWO | THREE | FOUR"

I don't know how useful this will be. But ... it was enjoyable to play with!

Notes

  1. This only accepts a narrow range of Objective-C expressions with a very naive parser
  2. One problem is that you can't import headers, so you will need to import headers you need manually in IRObjc.mm
  3. It's slower than a basic TurboModule call, obviously, because it has to do more work ... maybe 8x slower, but still quite fast
  4. Debugging syntax errors is hell
  5. The implementation is heavily vibe-coded and I know it's not very efficient, but w/e
  6. An astute observer may note that one of those expressions (addObject) is a void expression. I detect that and return the target so you can keep chaining. So it's not really even valid Objective-C, but ... it's useful 🤷
import { invokeObjC } from "./invoke"
function runSomeObjC() {
const s = "Hello, World!"
const result = invokeObjC(`[[NSString stringWithString:@"${s}"] uppercaseString]`)
console.log(result)
// Test array literal with method chaining
const result2 = invokeObjC(
`[[@[@"Apple", @"Banana", @"Cherry"] componentsJoinedByString:@", "] uppercaseString]`,
)
console.log({ result2 })
// Test nested method calls with array manipulation
const result3 = invokeObjC(
`[[[@[@"One", @"Two", @"Three"] mutableCopy] addObject:@"Four"] componentsJoinedByString:@" | "]`,
)
console.log({ result3 })
}
import IRObjc from "../specs/NativeIRObjc"
type ObjCArg = string | ObjCInput | ObjCLiteralArray
type ObjCLiteralArray = ["@", ...string[]]
type ObjCInput = [ObjCArg, string, ObjCArg[]]
export function invokeObjC(input: ObjCInput | string): string {
if (typeof input === "string") {
input = objcToArrayStructure(input)
}
const jsonString = JSON.stringify(input)
console.tron.log("JSON being sent:", jsonString)
const result = IRObjc.invokeObjC(jsonString)
return result
}
function objcToArrayStructure(input: string): ObjCInput {
// Parse nested Objective-C method calls like "[[NSUUID UUID] UUIDString]"
// into array structure: [["NSUUID", "UUID", []], "UUIDString", []]
input = input.trim()
if (!input.startsWith("[") || !input.endsWith("]")) {
throw new Error("Invalid Objective-C code: must be wrapped in brackets")
}
// Remove outer brackets
input = input.slice(1, -1).trim()
// Check for invalid semicolon usage (outside of strings)
if (containsSemicolonOutsideStrings(input)) {
throw new Error(
"Invalid Objective-C syntax: semicolons are not supported in method call expressions",
)
}
// Find the main method call structure
let depth = 0
let receiverEnd = -1
for (let i = 0; i < input.length; i++) {
if (input[i] === "[") depth++
else if (input[i] === "]") depth--
else if (depth === 0 && input[i] === " ") {
receiverEnd = i
break
}
}
if (receiverEnd === -1) {
throw new Error("Invalid Objective-C code: no method found")
}
const receiverPart = input.slice(0, receiverEnd).trim()
const methodPart = input.slice(receiverEnd + 1).trim()
// Parse receiver (could be a class name, nested method call, or array literal)
let receiver: ObjCArg
if (receiverPart.startsWith("@[")) {
// Array literal
receiver = parseArrayLiteral(receiverPart)
} else if (receiverPart.startsWith("[")) {
// Nested method call
receiver = objcToArrayStructure(receiverPart)
} else {
// Class name
receiver = receiverPart
}
// Parse method name and arguments
const { methodName, args } = parseMethodAndArgs(methodPart)
return [receiver, methodName, args]
}
function containsSemicolonOutsideStrings(input: string): boolean {
let inString = false
let escapeNext = false
for (let i = 0; i < input.length; i++) {
const char = input[i]
if (escapeNext) {
escapeNext = false
continue
}
if (char === "\\") {
escapeNext = true
continue
}
if (char === '"' && !escapeNext) {
inString = !inString
continue
}
if (!inString && char === ";") {
return true
}
}
return false
}
function parseArrayLiteral(input: string): ObjCLiteralArray {
// Parse @[@"Apple", @"Banana", @"Cherry"] into ["@", "Apple", "Banana", "Cherry"]
if (!input.startsWith("@[") || !input.endsWith("]")) {
throw new Error("Invalid array literal format")
}
const content = input.slice(2, -1).trim()
if (!content) {
return ["@"]
}
const elements: string[] = []
let depth = 0
let start = 0
let inString = false
let escapeNext = false
for (let i = 0; i < content.length; i++) {
const char = content[i]
if (escapeNext) {
escapeNext = false
continue
}
if (char === "\\") {
escapeNext = true
continue
}
if (char === '"' && !escapeNext) {
inString = !inString
continue
}
if (!inString) {
if (char === "[") depth++
else if (char === "]") depth--
else if (char === "," && depth === 0) {
const element = content.slice(start, i).trim()
elements.push(parseStringLiteral(element))
start = i + 1
}
}
}
// Add the last element
const lastElement = content.slice(start).trim()
if (lastElement) {
elements.push(parseStringLiteral(lastElement))
}
return ["@", ...elements]
}
function parseStringLiteral(input: string): string {
input = input.trim()
if (input.startsWith('@"') && input.endsWith('"')) {
return input.slice(2, -1)
}
return input
}
function parseMethodAndArgs(methodPart: string): { methodName: string; args: ObjCArg[] } {
// Handle method calls with complex arguments
const colonIndex = methodPart.indexOf(":")
if (colonIndex === -1) {
// No arguments
return { methodName: methodPart.trim(), args: [] }
}
const methodName = methodPart.slice(0, colonIndex + 1)
const argsString = methodPart.slice(colonIndex + 1).trim()
const args: ObjCArg[] = []
if (argsString) {
args.push(...parseArguments(argsString))
}
return { methodName, args }
}
function parseArguments(argsString: string): ObjCArg[] {
// For simple cases like stringWithString:@"Hello", there's just one argument
const trimmed = argsString.trim()
if (!trimmed) {
return []
}
// For now, handle the simple case of a single argument
// TODO: Handle multiple arguments separated by commas
return [parseArgument(trimmed)]
}
function parseArgument(arg: string): ObjCArg {
arg = arg.trim()
if (arg.startsWith('@"') && arg.endsWith('"')) {
// String literal
return arg.slice(2, -1)
} else if (arg.startsWith("@[")) {
// Array literal
return parseArrayLiteral(arg)
} else if (arg.startsWith("[")) {
// Nested method call
return objcToArrayStructure(arg)
}
return arg
}
#import <Foundation/Foundation.h>
#import <AppSpec/AppSpec.h>
@interface IRObjc : NativeIRObjcSpecBase <NativeIRObjcSpec>
@end
#import "IRObjc.h"
#include <Foundation/Foundation.h>
@implementation IRObjc RCT_EXPORT_MODULE()
// Invoke objective-c expression via an array structure
- (NSString *)invokeObjC:(NSString *)inputString {
// Parse the input string into an array
NSData *jsonData = [inputString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
NSArray *input = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error) {
NSLog(@"Error parsing input: %@", error);
return nil;
}
if (![input isKindOfClass:[NSArray class]] || input.count != 3) {
NSLog(@"Invalid input format. Expected array with 3 elements: [instanceCreation, methodName, arguments]");
NSLog(@"Input: %@", input);
return nil;
}
// Get the instance from the first element
id target = nil;
id firstElement = input[0];
if ([firstElement isKindOfClass:[NSArray class]]) {
NSArray *firstArray = (NSArray *)firstElement;
// Check if this is a literal array (starts with "@")
if (firstArray.count > 0 && [firstArray[0] isKindOfClass:[NSString class]] && [firstArray[0] isEqualToString:@"@"]) {
// Remove the "@" marker and use the rest as literal array target
NSRange range = NSMakeRange(1, firstArray.count - 1);
target = [firstArray subarrayWithRange:range];
} else {
// Create instance using nested method call
target = [self processMethodCall:firstElement];
if (!target) {
return nil;
}
}
} else if ([firstElement isKindOfClass:[NSString class]]) {
// First element is a class name, treat this as a class method call
NSString *className = (NSString *)firstElement;
Class targetClass = NSClassFromString(className);
if (!targetClass) {
NSLog(@"Class '%@' not found", className);
return nil;
}
target = targetClass;
} else {
NSLog(@"First element must be either a class name (NSString) or an array representing instance creation");
return nil;
}
// Process the method call on the instance
NSString *methodName = input[1];
NSArray *args = input[2];
if (![methodName isKindOfClass:[NSString class]] || ![args isKindOfClass:[NSArray class]]) {
NSLog(@"Invalid format for method call. Expected [NSString, NSArray]");
return nil;
}
SEL selector = NSSelectorFromString(methodName);
if (![target respondsToSelector:selector]) {
NSLog(@"Method '%@' not found on target '%@'", methodName, target);
return nil;
}
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
if (!signature) {
NSLog(@"Unable to get method signature for '%@'", methodName);
return nil;
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setTarget:target];
// Set arguments - use args count instead of method signature count
for (NSUInteger i = 0; i < args.count; i++) {
id arg = args[i];
if ([arg isKindOfClass:[NSArray class]]) {
NSArray *argArray = (NSArray *)arg;
// Check if this is a literal array (starts with "@")
if (argArray.count > 0 && [argArray[0] isKindOfClass:[NSString class]] && [argArray[0] isEqualToString:@"@"]) {
// Remove the "@" marker and use the rest as literal array
NSRange range = NSMakeRange(1, argArray.count - 1);
arg = [argArray subarrayWithRange:range];
} else if (argArray.count == 3 && [argArray[0] isKindOfClass:[NSString class]] && ![argArray[0] isEqualToString:@"@"]) {
// This looks like a method call [className, methodName, args] (not a literal array)
id result = [self processMethodCall:argArray];
arg = result;
} else {
// Recursively resolve nested calls
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:arg options:0 error:&error];
if (!error) {
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSString *result = [self invokeObjC:jsonString];
arg = result;
} else {
NSLog(@"Error serializing nested call: %@", error);
return nil;
}
}
}
[invocation setArgument:&arg atIndex:i + 2]; // +2 because 0=self, 1=_cmd
}
// Log what we're about to invoke
NSLog(@"Invoking: %@", input);
NSLog(@"Target: %@, Method: %@, Args: %@", target, methodName, args);
[invocation invoke];
// Get the return value
const char *returnType = [signature methodReturnType];
if (strcmp(returnType, @encode(void)) != 0) {
__unsafe_unretained id returnValue;
[invocation getReturnValue:&returnValue];
return [returnValue description];
}
return nil;
}
// Helper method to process array-based method calls
- (id)processMethodCall:(NSArray *)methodCall {
NSLog(@"processMethodCall called with: %@", methodCall);
if (![methodCall isKindOfClass:[NSArray class]] || methodCall.count != 3) {
NSLog(@"Invalid method call format. Expected array with 3 elements: [receiver, methodName, arguments]");
NSLog(@"Method call: %@", methodCall);
return nil;
}
id receiver = methodCall[0];
NSString *methodName = methodCall[1];
NSArray *args = methodCall[2];
if (![methodName isKindOfClass:[NSString class]] ||
![args isKindOfClass:[NSArray class]]) {
NSLog(@"Invalid method call format. Expected [receiver, NSString, NSArray]");
return nil;
}
// Handle different types of receivers
id target = nil;
if ([receiver isKindOfClass:[NSString class]]) {
// Simple class name
NSString *className = (NSString *)receiver;
Class targetClass = NSClassFromString(className);
if (!targetClass) {
NSLog(@"Class '%@' not found", className);
return nil;
}
target = targetClass;
} else if ([receiver isKindOfClass:[NSArray class]]) {
NSArray *receiverArray = (NSArray *)receiver;
// Check if this is a literal array (starts with "@")
if (receiverArray.count > 0 && [receiverArray[0] isKindOfClass:[NSString class]] && [receiverArray[0] isEqualToString:@"@"]) {
// Remove the "@" marker and use the rest as literal array
NSRange range = NSMakeRange(1, receiverArray.count - 1);
target = [receiverArray subarrayWithRange:range];
} else {
// Nested method call - recursively process it
target = [self processMethodCall:receiverArray];
if (!target) {
NSLog(@"Failed to process nested method call");
return nil;
}
}
} else {
NSLog(@"Invalid receiver type. Expected NSString or NSArray, got %@", [receiver class]);
return nil;
}
SEL selector = NSSelectorFromString(methodName);
if (![target respondsToSelector:selector]) {
NSLog(@"Method '%@' not found on target '%@'", methodName, target);
return nil;
}
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
if (!signature) {
NSLog(@"Unable to get method signature for '%@'", methodName);
return nil;
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setTarget:target];
// Process arguments - use args count instead of method signature count
for (NSUInteger i = 0; i < args.count; i++) {
id arg = args[i];
if ([arg isKindOfClass:[NSArray class]]) {
NSArray *argArray = (NSArray *)arg;
// Check if this is a literal array (starts with "@")
if (argArray.count > 0 && [argArray[0] isKindOfClass:[NSString class]] && [argArray[0] isEqualToString:@"@"]) {
// Remove the "@" marker and use the rest as literal array
NSRange range = NSMakeRange(1, argArray.count - 1);
arg = [argArray subarrayWithRange:range];
} else if (argArray.count == 3 && [argArray[0] isKindOfClass:[NSString class]] && ![argArray[0] isEqualToString:@"@"]) {
// This looks like a method call [className, methodName, args] (not a literal array)
id result = [self processMethodCall:argArray];
arg = result;
} else {
// Recursively resolve nested calls
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:arg options:0 error:&error];
if (!error) {
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSString *result = [self invokeObjC:jsonString];
arg = result;
} else {
NSLog(@"Error serializing nested call: %@", error);
return nil;
}
}
}
[invocation setArgument:&arg atIndex:i + 2]; // +2 because 0=self, 1=_cmd
}
[invocation invoke];
// Get return value
const char *returnType = [signature methodReturnType];
if (strcmp(returnType, @encode(void)) != 0) {
__unsafe_unretained id returnValue;
[invocation getReturnValue:&returnValue];
NSLog(@"processMethodCall returning: %@", returnValue);
return returnValue;
}
// For void methods, return the target object to allow method chaining
NSLog(@"processMethodCall returning target (void method): %@", target);
return target;
}
@end
import type { TurboModule } from "react-native"
import { TurboModuleRegistry } from "react-native"
export interface Spec extends TurboModule {
invokeObjC(input: string): string
}
export default TurboModuleRegistry.getEnforcing<Spec>("IRObjc")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment