UNPKG

frida-objc-bridge

Version:

Objective-C runtime interop from Frida

1,354 lines (1,199 loc) 94.9 kB
/* jshint esnext: true, evil: true */ import {getApi, defaultInvocationOptions} from './lib/api.js'; import * as fastpaths from './lib/fastpaths.js'; function Runtime() { const pointerSize = Process.pointerSize; let api = null; let apiError = null; const realizedClasses = new Set(); const classRegistry = new ClassRegistry(); const protocolRegistry = new ProtocolRegistry(); const replacedMethods = new Map(); const scheduledWork = new Map(); let nextId = 1; let workCallback = null; let NSAutoreleasePool = null; const bindings = new Map(); let readObjectIsa = null; const msgSendBySignatureId = new Map(); const msgSendSuperBySignatureId = new Map(); let cachedNSString = null; let cachedNSStringCtor = null; let cachedNSNumber = null; let cachedNSNumberCtor = null; let singularTypeById = null; let modifiers = null; try { tryInitialize(); } catch (e) { } function tryInitialize() { if (api !== null) return true; if (apiError !== null) throw apiError; try { api = getApi(); } catch (e) { apiError = e; throw e; } return api !== null; } function dispose() { for (const [rawMethodHandle, impls] of replacedMethods.entries()) { const methodHandle = ptr(rawMethodHandle); const [oldImp, newImp] = impls; if (api.method_getImplementation(methodHandle).equals(newImp)) api.method_setImplementation(methodHandle, oldImp); } replacedMethods.clear(); } Script.bindWeak(this, dispose); Object.defineProperty(this, 'available', { enumerable: true, get() { return tryInitialize(); } }); Object.defineProperty(this, 'api', { enumerable: true, get() { return getApi(); } }); Object.defineProperty(this, 'classes', { enumerable: true, value: classRegistry }); Object.defineProperty(this, 'protocols', { enumerable: true, value: protocolRegistry }); Object.defineProperty(this, 'Object', { enumerable: true, value: ObjCObject }); Object.defineProperty(this, 'Protocol', { enumerable: true, value: ObjCProtocol }); Object.defineProperty(this, 'Block', { enumerable: true, value: Block }); Object.defineProperty(this, 'mainQueue', { enumerable: true, get() { return api?._dispatch_main_q ?? null; } }); Object.defineProperty(this, 'registerProxy', { enumerable: true, value: registerProxy }); Object.defineProperty(this, 'registerClass', { enumerable: true, value: registerClass }); Object.defineProperty(this, 'registerProtocol', { enumerable: true, value: registerProtocol }); Object.defineProperty(this, 'bind', { enumerable: true, value: bind }); Object.defineProperty(this, 'unbind', { enumerable: true, value: unbind }); Object.defineProperty(this, 'getBoundData', { enumerable: true, value: getBoundData }); Object.defineProperty(this, 'enumerateLoadedClasses', { enumerable: true, value: enumerateLoadedClasses }); Object.defineProperty(this, 'enumerateLoadedClassesSync', { enumerable: true, value: enumerateLoadedClassesSync }); Object.defineProperty(this, 'choose', { enumerable: true, value: choose }); Object.defineProperty(this, 'chooseSync', { enumerable: true, value(specifier) { const instances = []; choose(specifier, { onMatch(i) { instances.push(i); }, onComplete() { } }); return instances; } }); this.schedule = function (queue, work) { const id = ptr(nextId++); scheduledWork.set(id.toString(), work); if (workCallback === null) { workCallback = new NativeCallback(performScheduledWorkItem, 'void', ['pointer']); } Script.pin(); api.dispatch_async_f(queue, id, workCallback); }; function performScheduledWorkItem(rawId) { const id = rawId.toString(); const work = scheduledWork.get(id); scheduledWork.delete(id); if (NSAutoreleasePool === null) NSAutoreleasePool = classRegistry.NSAutoreleasePool; const pool = NSAutoreleasePool.alloc().init(); let pendingException = null; try { work(); } catch (e) { pendingException = e; } pool.release(); setImmediate(performScheduledWorkCleanup, pendingException); } function performScheduledWorkCleanup(pendingException) { Script.unpin(); if (pendingException !== null) { throw pendingException; } } this.implement = function (method, fn) { return new NativeCallback(fn, method.returnType, method.argumentTypes); }; this.selector = selector; this.selectorAsString = selectorAsString; function selector(name) { return api.sel_registerName(Memory.allocUtf8String(name)); } function selectorAsString(sel) { return api.sel_getName(sel).readUtf8String(); } const registryBuiltins = new Set([ "prototype", "constructor", "hasOwnProperty", "toJSON", "toString", "valueOf" ]); function ClassRegistry() { const cachedClasses = {}; let numCachedClasses = 0; const registry = new Proxy(this, { has(target, property) { return hasProperty(property); }, get(target, property, receiver) { switch (property) { case "prototype": return target.prototype; case "constructor": return target.constructor; case "hasOwnProperty": return hasProperty; case "toJSON": return toJSON; case "toString": return toString; case "valueOf": return valueOf; default: const klass = findClass(property); return (klass !== null) ? klass : undefined; } }, set(target, property, value, receiver) { return false; }, ownKeys(target) { if (api === null) return []; let numClasses = api.objc_getClassList(NULL, 0); if (numClasses !== numCachedClasses) { // It's impossible to unregister classes in ObjC, so if the number of // classes hasn't changed, we can assume that the list is up to date. const classHandles = Memory.alloc(numClasses * pointerSize); numClasses = api.objc_getClassList(classHandles, numClasses); for (let i = 0; i !== numClasses; i++) { const handle = classHandles.add(i * pointerSize).readPointer(); const name = api.class_getName(handle).readUtf8String(); cachedClasses[name] = handle; } numCachedClasses = numClasses; } return Object.keys(cachedClasses); }, getOwnPropertyDescriptor(target, property) { return { writable: false, configurable: true, enumerable: true }; }, }); function hasProperty(name) { if (registryBuiltins.has(name)) return true; return findClass(name) !== null; } function getClass(name) { const cls = findClass(name); if (cls === null) throw new Error("Unable to find class '" + name + "'"); return cls; } function findClass(name) { let handle = cachedClasses[name]; if (handle === undefined) { handle = api.objc_lookUpClass(Memory.allocUtf8String(name)); if (handle.isNull()) return null; cachedClasses[name] = handle; numCachedClasses++; } return new ObjCObject(handle, undefined, true); } function toJSON() { return Object.keys(registry).reduce(function (r, name) { r[name] = getClass(name).toJSON(); return r; }, {}); } function toString() { return "ClassRegistry"; } function valueOf() { return "ClassRegistry"; } return registry; } function ProtocolRegistry() { let cachedProtocols = {}; let numCachedProtocols = 0; const registry = new Proxy(this, { has(target, property) { return hasProperty(property); }, get(target, property, receiver) { switch (property) { case "prototype": return target.prototype; case "constructor": return target.constructor; case "hasOwnProperty": return hasProperty; case "toJSON": return toJSON; case "toString": return toString; case "valueOf": return valueOf; default: const proto = findProtocol(property); return (proto !== null) ? proto : undefined; } }, set(target, property, value, receiver) { return false; }, ownKeys(target) { if (api === null) return []; const numProtocolsBuf = Memory.alloc(pointerSize); const protocolHandles = api.objc_copyProtocolList(numProtocolsBuf); try { const numProtocols = numProtocolsBuf.readUInt(); if (numProtocols !== numCachedProtocols) { cachedProtocols = {}; for (let i = 0; i !== numProtocols; i++) { const handle = protocolHandles.add(i * pointerSize).readPointer(); const name = api.protocol_getName(handle).readUtf8String(); cachedProtocols[name] = handle; } numCachedProtocols = numProtocols; } } finally { api.free(protocolHandles); } return Object.keys(cachedProtocols); }, getOwnPropertyDescriptor(target, property) { return { writable: false, configurable: true, enumerable: true }; }, }); function hasProperty(name) { if (registryBuiltins.has(name)) return true; return findProtocol(name) !== null; } function findProtocol(name) { let handle = cachedProtocols[name]; if (handle === undefined) { handle = api.objc_getProtocol(Memory.allocUtf8String(name)); if (handle.isNull()) return null; cachedProtocols[name] = handle; numCachedProtocols++; } return new ObjCProtocol(handle); } function toJSON() { return Object.keys(registry).reduce(function (r, name) { r[name] = { handle: cachedProtocols[name] }; return r; }, {}); } function toString() { return "ProtocolRegistry"; } function valueOf() { return "ProtocolRegistry"; } return registry; } const objCObjectBuiltins = new Set([ "prototype", "constructor", "handle", "hasOwnProperty", "toJSON", "toString", "valueOf", "equals", "$kind", "$super", "$superClass", "$class", "$className", "$moduleName", "$protocols", "$methods", "$ownMethods", "$ivars" ]); function ObjCObject(handle, protocol, cachedIsClass, superSpecifier) { let cachedClassHandle = null; let cachedKind = null; let cachedSuper = null; let cachedSuperClass = null; let cachedClass = null; let cachedClassName = null; let cachedModuleName = null; let cachedProtocols = null; let cachedMethodNames = null; let cachedProtocolMethods = null; let respondsToSelector = null; const cachedMethods = {}; let cachedNativeMethodNames = null; let cachedOwnMethodNames = null; let cachedIvars = null; handle = getHandle(handle); if (cachedIsClass === undefined) { // We need to ensure the class is realized, otherwise calling APIs like object_isClass() will crash. // The first message delivery will realize the class, but users intercepting calls to objc_msgSend() // and inspecting the first argument will run into this situation. const klass = api.object_getClass(handle); const key = klass.toString(); if (!realizedClasses.has(key)) { api.objc_lookUpClass(api.class_getName(klass)); realizedClasses.add(key); } } const self = new Proxy(this, { has(target, property) { return hasProperty(property); }, get(target, property, receiver) { switch (property) { case "handle": return handle; case "prototype": return target.prototype; case "constructor": return target.constructor; case "hasOwnProperty": return hasProperty; case "toJSON": return toJSON; case "toString": case "valueOf": const descriptionImpl = receiver.description; if (descriptionImpl !== undefined) { const description = descriptionImpl.call(receiver); if (description !== null) return description.UTF8String.bind(description); } return function () { return receiver.$className; }; case "equals": return equals; case "$kind": if (cachedKind === null) { if (isClass()) cachedKind = api.class_isMetaClass(handle) ? 'meta-class' : 'class'; else cachedKind = 'instance'; } return cachedKind; case "$super": if (cachedSuper === null) { const superHandle = api.class_getSuperclass(classHandle()); if (!superHandle.isNull()) { const specifier = Memory.alloc(2 * pointerSize); specifier.writePointer(handle); specifier.add(pointerSize).writePointer(superHandle); cachedSuper = [new ObjCObject(handle, undefined, cachedIsClass, specifier)]; } else { cachedSuper = [null]; } } return cachedSuper[0]; case "$superClass": if (cachedSuperClass === null) { const superClassHandle = api.class_getSuperclass(classHandle()); if (!superClassHandle.isNull()) { cachedSuperClass = [new ObjCObject(superClassHandle)]; } else { cachedSuperClass = [null]; } } return cachedSuperClass[0]; case "$class": if (cachedClass === null) cachedClass = new ObjCObject(api.object_getClass(handle), undefined, true); return cachedClass; case "$className": if (cachedClassName === null) { if (superSpecifier) cachedClassName = api.class_getName(superSpecifier.add(pointerSize).readPointer()).readUtf8String(); else if (isClass()) cachedClassName = api.class_getName(handle).readUtf8String(); else cachedClassName = api.object_getClassName(handle).readUtf8String(); } return cachedClassName; case "$moduleName": if (cachedModuleName === null) { cachedModuleName = api.class_getImageName(classHandle()).readUtf8String(); } return cachedModuleName; case "$protocols": if (cachedProtocols === null) { cachedProtocols = {}; const numProtocolsBuf = Memory.alloc(pointerSize); const protocolHandles = api.class_copyProtocolList(classHandle(), numProtocolsBuf); if (!protocolHandles.isNull()) { try { const numProtocols = numProtocolsBuf.readUInt(); for (let i = 0; i !== numProtocols; i++) { const protocolHandle = protocolHandles.add(i * pointerSize).readPointer(); const p = new ObjCProtocol(protocolHandle); cachedProtocols[p.name] = p; } } finally { api.free(protocolHandles); } } } return cachedProtocols; case "$methods": if (cachedNativeMethodNames === null) { const klass = superSpecifier ? superSpecifier.add(pointerSize).readPointer() : classHandle(); const meta = api.object_getClass(klass); const names = new Set(); let cur = meta; do { for (let methodName of collectMethodNames(cur, "+ ")) names.add(methodName); cur = api.class_getSuperclass(cur); } while (!cur.isNull()); cur = klass; do { for (let methodName of collectMethodNames(cur, "- ")) names.add(methodName); cur = api.class_getSuperclass(cur); } while (!cur.isNull()); cachedNativeMethodNames = Array.from(names); } return cachedNativeMethodNames; case "$ownMethods": if (cachedOwnMethodNames === null) { const klass = superSpecifier ? superSpecifier.add(pointerSize).readPointer() : classHandle(); const meta = api.object_getClass(klass); const classMethods = collectMethodNames(meta, "+ "); const instanceMethods = collectMethodNames(klass, "- "); cachedOwnMethodNames = classMethods.concat(instanceMethods); } return cachedOwnMethodNames; case "$ivars": if (cachedIvars === null) { if (isClass()) cachedIvars = {}; else cachedIvars = new ObjCIvars(self, classHandle()); } return cachedIvars; default: if (typeof property === "symbol") { return target[property]; } if (protocol) { const details = findProtocolMethod(property); if (details === null || !details.implemented) return undefined; } const wrapper = findMethodWrapper(property); if (wrapper === null) return undefined; return wrapper; } }, set(target, property, value, receiver) { return false; }, ownKeys(target) { if (cachedMethodNames === null) { if (!protocol) { const jsNames = {}; const nativeNames = {}; let cur = api.object_getClass(handle); do { const numMethodsBuf = Memory.alloc(pointerSize); const methodHandles = api.class_copyMethodList(cur, numMethodsBuf); const fullNamePrefix = isClass() ? "+ " : "- "; try { const numMethods = numMethodsBuf.readUInt(); for (let i = 0; i !== numMethods; i++) { const methodHandle = methodHandles.add(i * pointerSize).readPointer(); const sel = api.method_getName(methodHandle); const nativeName = api.sel_getName(sel).readUtf8String(); if (nativeNames[nativeName] !== undefined) continue; nativeNames[nativeName] = nativeName; const jsName = jsMethodName(nativeName); let serial = 2; let name = jsName; while (jsNames[name] !== undefined) { serial++; name = jsName + serial; } jsNames[name] = true; const fullName = fullNamePrefix + nativeName; if (cachedMethods[fullName] === undefined) { const details = { sel: sel, handle: methodHandle, wrapper: null }; cachedMethods[fullName] = details; cachedMethods[name] = details; } } } finally { api.free(methodHandles); } cur = api.class_getSuperclass(cur); } while (!cur.isNull()); cachedMethodNames = Object.keys(jsNames); } else { const methodNames = []; const protocolMethods = allProtocolMethods(); Object.keys(protocolMethods).forEach(function (methodName) { if (methodName[0] !== '+' && methodName[0] !== '-') { const details = protocolMethods[methodName]; if (details.implemented) { methodNames.push(methodName); } } }); cachedMethodNames = methodNames; } } return ['handle'].concat(cachedMethodNames); }, getOwnPropertyDescriptor(target, property) { return { writable: false, configurable: true, enumerable: true }; }, }); if (protocol) { respondsToSelector = !isClass() ? findMethodWrapper("- respondsToSelector:") : null; } return self; function hasProperty(name) { if (objCObjectBuiltins.has(name)) return true; if (protocol) { const details = findProtocolMethod(name); return !!(details !== null && details.implemented); } return findMethod(name) !== null; } function classHandle() { if (cachedClassHandle === null) cachedClassHandle = isClass() ? handle : api.object_getClass(handle); return cachedClassHandle; } function isClass() { if (cachedIsClass === undefined) { if (api.object_isClass) cachedIsClass = !!api.object_isClass(handle); else cachedIsClass = !!api.class_isMetaClass(api.object_getClass(handle)); } return cachedIsClass; } function findMethod(rawName) { let method = cachedMethods[rawName]; if (method !== undefined) return method; const tokens = parseMethodName(rawName); const fullName = tokens[2]; method = cachedMethods[fullName]; if (method !== undefined) { cachedMethods[rawName] = method; return method; } const kind = tokens[0]; const name = tokens[1]; const sel = selector(name); const defaultKind = isClass() ? '+' : '-'; if (protocol) { const details = findProtocolMethod(fullName); if (details !== null) { method = { sel: sel, types: details.types, wrapper: null, kind }; } } if (method === undefined) { const methodHandle = (kind === '+') ? api.class_getClassMethod(classHandle(), sel) : api.class_getInstanceMethod(classHandle(), sel); if (!methodHandle.isNull()) { method = { sel: sel, handle: methodHandle, wrapper: null, kind }; } else { if (isClass() || kind !== '-' || name === "forwardingTargetForSelector:" || name === "methodSignatureForSelector:") { return null; } let target = self; if ("- forwardingTargetForSelector:" in self) { const forwardingTarget = self.forwardingTargetForSelector_(sel); if (forwardingTarget !== null && forwardingTarget.$kind === 'instance') { target = forwardingTarget; } else { return null; } } else { return null; } const methodHandle = api.class_getInstanceMethod(api.object_getClass(target.handle), sel); if (methodHandle.isNull()) { return null; } let types = api.method_getTypeEncoding(methodHandle).readUtf8String(); if (types === null || types === "") { types = stealTypesFromProtocols(target, fullName); if (types === null) types = stealTypesFromProtocols(self, fullName); if (types === null) return null; } method = { sel, types, wrapper: null, kind }; } } cachedMethods[fullName] = method; cachedMethods[rawName] = method; if (kind === defaultKind) cachedMethods[jsMethodName(name)] = method; return method; } function stealTypesFromProtocols(klass, fullName) { const candidates = Object.keys(klass.$protocols) .map(protocolName => flatProtocolMethods({}, klass.$protocols[protocolName])) .reduce((allMethods, methods) => { Object.assign(allMethods, methods); return allMethods; }, {}); const method = candidates[fullName]; if (method === undefined) { return null; } return method.types; } function flatProtocolMethods(result, protocol) { if (protocol.methods !== undefined) { Object.assign(result, protocol.methods); } if (protocol.protocol !== undefined) { flatProtocolMethods(result, protocol.protocol); } return result; } function findProtocolMethod(rawName) { const protocolMethods = allProtocolMethods(); const details = protocolMethods[rawName]; return (details !== undefined) ? details : null; } function allProtocolMethods() { if (cachedProtocolMethods === null) { const methods = {}; const protocols = collectProtocols(protocol); const defaultKind = isClass() ? '+' : '-'; Object.keys(protocols).forEach(function (name) { const p = protocols[name]; const m = p.methods; Object.keys(m).forEach(function (fullMethodName) { const method = m[fullMethodName]; const methodName = fullMethodName.substr(2); const kind = fullMethodName[0]; let didCheckImplemented = false; let implemented = false; const details = { types: method.types }; Object.defineProperty(details, 'implemented', { get() { if (!didCheckImplemented) { if (method.required) { implemented = true; } else { implemented = (respondsToSelector !== null && respondsToSelector.call(self, selector(methodName))); } didCheckImplemented = true; } return implemented; } }); methods[fullMethodName] = details; if (kind === defaultKind) methods[jsMethodName(methodName)] = details; }); }); cachedProtocolMethods = methods; } return cachedProtocolMethods; } function findMethodWrapper(name) { const method = findMethod(name); if (method === null) return null; let wrapper = method.wrapper; if (wrapper === null) { wrapper = makeMethodInvocationWrapper(method, self, superSpecifier, defaultInvocationOptions); method.wrapper = wrapper; } return wrapper; } function parseMethodName(rawName) { const match = /([+\-])\s(\S+)/.exec(rawName); let name, kind; if (match === null) { kind = isClass() ? '+' : '-'; name = objcMethodName(rawName); } else { kind = match[1]; name = match[2]; } const fullName = [kind, name].join(' '); return [kind, name, fullName]; } function toJSON() { return { handle: handle.toString() }; } function equals(ptr) { return handle.equals(getHandle(ptr)); } } function getReplacementMethodImplementation(methodHandle) { const existingEntry = replacedMethods.get(methodHandle.toString()); if (existingEntry === undefined) return null; const [, newImp] = existingEntry; return newImp; } function replaceMethodImplementation(methodHandle, imp) { const key = methodHandle.toString(); let oldImp; const existingEntry = replacedMethods.get(key); if (existingEntry !== undefined) [oldImp] = existingEntry; else oldImp = api.method_getImplementation(methodHandle); if (!imp.equals(oldImp)) replacedMethods.set(key, [oldImp, imp]); else replacedMethods.delete(key); api.method_setImplementation(methodHandle, imp); } function collectMethodNames(klass, prefix) { const names = []; const numMethodsBuf = Memory.alloc(pointerSize); const methodHandles = api.class_copyMethodList(klass, numMethodsBuf); try { const numMethods = numMethodsBuf.readUInt(); for (let i = 0; i !== numMethods; i++) { const methodHandle = methodHandles.add(i * pointerSize).readPointer(); const sel = api.method_getName(methodHandle); const nativeName = api.sel_getName(sel).readUtf8String(); names.push(prefix + nativeName); } } finally { api.free(methodHandles); } return names; } function ObjCProtocol(handle) { let cachedName = null; let cachedProtocols = null; let cachedProperties = null; let cachedMethods = null; Object.defineProperty(this, 'handle', { value: handle, enumerable: true }); Object.defineProperty(this, 'name', { get() { if (cachedName === null) cachedName = api.protocol_getName(handle).readUtf8String(); return cachedName; }, enumerable: true }); Object.defineProperty(this, 'protocols', { get() { if (cachedProtocols === null) { cachedProtocols = {}; const numProtocolsBuf = Memory.alloc(pointerSize); const protocolHandles = api.protocol_copyProtocolList(handle, numProtocolsBuf); if (!protocolHandles.isNull()) { try { const numProtocols = numProtocolsBuf.readUInt(); for (let i = 0; i !== numProtocols; i++) { const protocolHandle = protocolHandles.add(i * pointerSize).readPointer(); const protocol = new ObjCProtocol(protocolHandle); cachedProtocols[protocol.name] = protocol; } } finally { api.free(protocolHandles); } } } return cachedProtocols; }, enumerable: true }); Object.defineProperty(this, 'properties', { get() { if (cachedProperties === null) { cachedProperties = {}; const numBuf = Memory.alloc(pointerSize); const propertyHandles = api.protocol_copyPropertyList(handle, numBuf); if (!propertyHandles.isNull()) { try { const numProperties = numBuf.readUInt(); for (let i = 0; i !== numProperties; i++) { const propertyHandle = propertyHandles.add(i * pointerSize).readPointer(); const propName = api.property_getName(propertyHandle).readUtf8String(); const attributes = {}; const attributeEntries = api.property_copyAttributeList(propertyHandle, numBuf); if (!attributeEntries.isNull()) { try { const numAttributeValues = numBuf.readUInt(); for (let j = 0; j !== numAttributeValues; j++) { const attributeEntry = attributeEntries.add(j * (2 * pointerSize)); const name = attributeEntry.readPointer().readUtf8String(); const value = attributeEntry.add(pointerSize).readPointer().readUtf8String(); attributes[name] = value; } } finally { api.free(attributeEntries); } } cachedProperties[propName] = attributes; } } finally { api.free(propertyHandles); } } } return cachedProperties; }, enumerable: true }); Object.defineProperty(this, 'methods', { get() { if (cachedMethods === null) { cachedMethods = {}; const numBuf = Memory.alloc(pointerSize); collectMethods(cachedMethods, numBuf, { required: true, instance: false }); collectMethods(cachedMethods, numBuf, { required: false, instance: false }); collectMethods(cachedMethods, numBuf, { required: true, instance: true }); collectMethods(cachedMethods, numBuf, { required: false, instance: true }); } return cachedMethods; }, enumerable: true }); function collectMethods(methods, numBuf, spec) { const methodDescValues = api.protocol_copyMethodDescriptionList(handle, spec.required ? 1 : 0, spec.instance ? 1 : 0, numBuf); if (methodDescValues.isNull()) return; try { const numMethodDescValues = numBuf.readUInt(); for (let i = 0; i !== numMethodDescValues; i++) { const methodDesc = methodDescValues.add(i * (2 * pointerSize)); const name = (spec.instance ? '- ' : '+ ') + selectorAsString(methodDesc.readPointer()); const types = methodDesc.add(pointerSize).readPointer().readUtf8String(); methods[name] = { required: spec.required, types: types }; } } finally { api.free(methodDescValues); } } } const objCIvarsBuiltins = new Set([ "prototype", "constructor", "hasOwnProperty", "toJSON", "toString", "valueOf" ]); function ObjCIvars(instance, classHandle) { const ivars = {}; let cachedIvarNames = null; let classHandles = []; let currentClassHandle = classHandle; do { classHandles.unshift(currentClassHandle); currentClassHandle = api.class_getSuperclass(currentClassHandle); } while (!currentClassHandle.isNull()); const numIvarsBuf = Memory.alloc(pointerSize); classHandles.forEach(c => { const ivarHandles = api.class_copyIvarList(c, numIvarsBuf); try { const numIvars = numIvarsBuf.readUInt(); for (let i = 0; i !== numIvars; i++) { const handle = ivarHandles.add(i * pointerSize).readPointer(); const name = api.ivar_getName(handle).readUtf8String(); ivars[name] = [handle, null]; } } finally { api.free(ivarHandles); } }); const self = new Proxy(this, { has(target, property) { return hasProperty(property); }, get(target, property, receiver) { switch (property) { case "prototype": return target.prototype; case "constructor": return target.constructor; case "hasOwnProperty": return hasProperty; case "toJSON": return toJSON; case "toString": return toString; case "valueOf": return valueOf; default: const ivar = findIvar(property); if (ivar === null) return undefined; return ivar.get(); } }, set(target, property, value, receiver) { const ivar = findIvar(property); if (ivar === null) throw new Error("Unknown ivar"); ivar.set(value); return true; }, ownKeys(target) { if (cachedIvarNames === null) cachedIvarNames = Object.keys(ivars); return cachedIvarNames; }, getOwnPropertyDescriptor(target, property) { return { writable: true, configurable: true, enumerable: true }; }, }); return self; function findIvar(name) { const entry = ivars[name]; if (entry === undefined) return null; let impl = entry[1]; if (impl === null) { const ivar = entry[0]; const offset = api.ivar_getOffset(ivar).toInt32(); const address = instance.handle.add(offset); const type = parseType(api.ivar_getTypeEncoding(ivar).readUtf8String()); const fromNative = type.fromNative || identityTransform; const toNative = type.toNative || identityTransform; let read, write; if (name === 'isa') { read = readObjectIsa; write = function () { throw new Error('Unable to set the isa instance variable'); }; } else { read = type.read; write = type.write; } impl = { get() { return fromNative.call(instance, read(address)); }, set(value) { write(address, toNative.call(instance, value)); } }; entry[1] = impl; } return impl; } function hasProperty(name) { if (objCIvarsBuiltins.has(name)) return true; return ivars.hasOwnProperty(name); } function toJSON() { return Object.keys(self).reduce(function (result, name) { result[name] = self[name]; return result; }, {}); } function toString() { return "ObjCIvars"; } function valueOf() { return "ObjCIvars"; } } let blockDescriptorAllocSize, blockDescriptorDeclaredSize, blockDescriptorOffsets; let blockSize, blockOffsets; if (pointerSize === 4) { blockDescriptorAllocSize = 16; /* sizeof (BlockDescriptor) == 12 */ blockDescriptorDeclaredSize = 20; blockDescriptorOffsets = { reserved: 0, size: 4, rest: 8 }; blockSize = 20; blockOffsets = { isa: 0, flags: 4, reserved: 8, invoke: 12, descriptor: 16 }; } else { blockDescriptorAllocSize = 32; /* sizeof (BlockDescriptor) == 24 */ blockDescriptorDeclaredSize = 32; blockDescriptorOffsets = { reserved: 0, size: 8, rest: 16 }; blockSize = 32; blockOffsets = { isa: 0, flags: 8, reserved: 12, invoke: 16, descriptor: 24 }; } const BLOCK_HAS_COPY_DISPOSE = (1 << 25); const BLOCK_HAS_CTOR = (1 << 26); const BLOCK_IS_GLOBAL = (1 << 28); const BLOCK_HAS_STRET = (1 << 29); const BLOCK_HAS_SIGNATURE = (1 << 30); function Block(target, options = defaultInvocationOptions) { this._options = options; if (target instanceof NativePointer) { const descriptor = target.add(blockOffsets.descriptor).readPointer(); this.handle = target; const flags = target.add(blockOffsets.flags).readU32(); if ((flags & BLOCK_HAS_SIGNATURE) !== 0) { const signatureOffset = ((flags & BLOCK_HAS_COPY_DISPOSE) !== 0) ? 2 : 0; this.types = descriptor.add(blockDescriptorOffsets.rest + (signatureOffset * pointerSize)).readPointer().readCString(); this._signature = parseSignature(this.types); } else { this._signature = null; } } else { this.declare(target); const descriptor = Memory.alloc(blockDescriptorAllocSize + blockSize); const block = descriptor.add(blockDescriptorAllocSize); const typesStr = Memory.allocUtf8String(this.types); descriptor.add(blockDescriptorOffsets.reserved).writeULong(0); descriptor.add(blockDescriptorOffsets.size).writeULong(blockDescriptorDeclaredSize); descriptor.add(blockDescriptorOffsets.rest).writePointer(typesStr); block.add(blockOffsets.isa).writePointer(classRegistry.__NSGlobalBlock__); block.add(blockOffsets.flags).writeU32(BLOCK_HAS_SIGNATURE | BLOCK_IS_GLOBAL); block.add(blockOffsets.reserved).writeU32(0); block.add(blockOffsets.descriptor).writePointer(descriptor); this.handle = block; this._storage = [descriptor, typesStr]; this.implementation = target.implementation; } } Object.defineProperties(Block.prototype, { implementation: { enumerable: true, get() { const address = this.handle.add(blockOffsets.invoke).readPointer().strip(); const signature = this._getSignature(); return makeBlockInvocationWrapper(this, signature, new NativeFunction( a