UNPKG

frida-java-bridge

Version:
1,892 lines (1,632 loc) 61 kB
import Env from './env.js'; import * as android from './android.js'; import { ensureClassInitialized as jvmEnsureClassInitialized, makeMethodMangler as jvmMakeMethodMangler } from './jvm.js'; import ClassModel from './class-model.js'; import LRU from './lru.js'; import mkdex from './mkdex.js'; import { getType, getPrimitiveType, getArrayType, makeJniObjectTypeName } from './types.js'; const jsizeSize = 4; let { ensureClassInitialized, makeMethodMangler } = android; const kAccStatic = 0x0008; const CONSTRUCTOR_METHOD = 1; const STATIC_METHOD = 2; const INSTANCE_METHOD = 3; const STATIC_FIELD = 1; const INSTANCE_FIELD = 2; const STRATEGY_VIRTUAL = 1; const STRATEGY_DIRECT = 2; const PENDING_USE = Symbol('PENDING_USE'); const DEFAULT_CACHE_DIR = '/data/local/tmp'; const { getCurrentThreadId, pointerSize } = Process; const factoryCache = { state: 'empty', factories: [], loaders: null, Integer: null }; let vm = null; let api = null; let isArtVm = null; let wrapperHandler = null; let dispatcherPrototype = null; let methodPrototype = null; let valueOfPrototype = null; let cachedLoaderInvoke = null; let cachedLoaderMethod = null; const ignoredThreads = new Map(); export default class ClassFactory { static _initialize (_vm, _api) { vm = _vm; api = _api; isArtVm = _api.flavor === 'art'; if (_api.flavor === 'jvm') { ensureClassInitialized = jvmEnsureClassInitialized; makeMethodMangler = jvmMakeMethodMangler; } } static _disposeAll (env) { factoryCache.factories.forEach(factory => { factory._dispose(env); }); } static get (classLoader) { const cache = getFactoryCache(); const defaultFactory = cache.factories[0]; if (classLoader === null) { return defaultFactory; } const indexObj = cache.loaders.get(classLoader); if (indexObj !== null) { const index = defaultFactory.cast(indexObj, cache.Integer); return cache.factories[index.intValue()]; } const factory = new ClassFactory(); factory.loader = classLoader; factory.cacheDir = defaultFactory.cacheDir; addFactoryToCache(factory, classLoader); return factory; } constructor () { this.cacheDir = DEFAULT_CACHE_DIR; this.codeCacheDir = DEFAULT_CACHE_DIR + '/dalvik-cache'; this.tempFileNaming = { prefix: 'frida', suffix: '' }; this._classes = {}; this._classHandles = new LRU(10, releaseClassHandle); this._patchedMethods = new Set(); this._loader = null; this._types = [{}, {}]; factoryCache.factories.push(this); } _dispose (env) { Array.from(this._patchedMethods).forEach(method => { method.implementation = null; }); this._patchedMethods.clear(); android.revertGlobalPatches(); this._classHandles.dispose(env); this._classes = {}; } get loader () { return this._loader; } set loader (value) { const isInitial = this._loader === null && value !== null; this._loader = value; if (isInitial && factoryCache.state === 'ready' && this === factoryCache.factories[0]) { addFactoryToCache(this, value); } } use (className, options = {}) { const allowCached = options.cache !== 'skip'; let C = allowCached ? this._getUsedClass(className) : undefined; if (C === undefined) { try { const env = vm.getEnv(); const { _loader: loader } = this; const getClassHandle = (loader !== null) ? makeLoaderClassHandleGetter(className, loader, env) : makeBasicClassHandleGetter(className); C = this._make(className, getClassHandle, env); } finally { if (allowCached) { this._setUsedClass(className, C); } } } return C; } _getUsedClass (className) { let c; while ((c = this._classes[className]) === PENDING_USE) { Thread.sleep(0.05); } if (c === undefined) { this._classes[className] = PENDING_USE; } return c; } _setUsedClass (className, c) { if (c !== undefined) { this._classes[className] = c; } else { delete this._classes[className]; } } _make (name, getClassHandle, env) { const C = makeClassWrapperConstructor(); const proto = Object.create(Wrapper.prototype, { [Symbol.for('n')]: { value: name }, $n: { get () { return this[Symbol.for('n')]; } }, [Symbol.for('C')]: { value: C }, $C: { get () { return this[Symbol.for('C')]; } }, [Symbol.for('w')]: { value: null, writable: true }, $w: { get () { return this[Symbol.for('w')]; }, set (val) { this[Symbol.for('w')] = val; } }, [Symbol.for('_s')]: { writable: true }, $_s: { get () { return this[Symbol.for('_s')]; }, set (val) { this[Symbol.for('_s')] = val; } }, [Symbol.for('c')]: { value: [null] }, $c: { get () { return this[Symbol.for('c')]; } }, [Symbol.for('m')]: { value: new Map() }, $m: { get () { return this[Symbol.for('m')]; } }, [Symbol.for('l')]: { value: null, writable: true }, $l: { get () { return this[Symbol.for('l')]; }, set (val) { this[Symbol.for('l')] = val; } }, [Symbol.for('gch')]: { value: getClassHandle }, $gch: { get () { return this[Symbol.for('gch')]; } }, [Symbol.for('f')]: { value: this }, $f: { get () { return this[Symbol.for('f')]; } } }); C.prototype = proto; const classWrapper = new C(null); proto[Symbol.for('w')] = classWrapper; proto.$w = classWrapper; const h = classWrapper.$borrowClassHandle(env); try { const classHandle = h.value; ensureClassInitialized(env, classHandle); proto.$l = ClassModel.build(classHandle, env); } finally { h.unref(env); } return classWrapper; } retain (obj) { const env = vm.getEnv(); return obj.$clone(env); } cast (obj, klass, owned) { const env = vm.getEnv(); let handle = obj.$h; if (handle === undefined) { handle = obj; } const h = klass.$borrowClassHandle(env); try { const isValidCast = env.isInstanceOf(handle, h.value); if (!isValidCast) { throw new Error(`Cast from '${env.getObjectClassName(handle)}' to '${klass.$n}' isn't possible`); } } finally { h.unref(env); } const C = klass.$C; return new C(handle, STRATEGY_VIRTUAL, env, owned); } wrap (handle, klass, env) { const C = klass.$C; const wrapper = new C(handle, STRATEGY_VIRTUAL, env, false); wrapper.$r = Script.bindWeak(wrapper, vm.makeHandleDestructor(handle)); return wrapper; } array (type, elements) { const env = vm.getEnv(); const primitiveType = getPrimitiveType(type); if (primitiveType !== null) { type = primitiveType.name; } const arrayType = getArrayType('[' + type, false, this); const rawArray = arrayType.toJni(elements, env); return arrayType.fromJni(rawArray, env, true); } registerClass (spec) { const env = vm.getEnv(); const tempHandles = []; try { const Class = this.use('java.lang.Class'); const Method = env.javaLangReflectMethod(); const invokeObjectMethodNoArgs = env.vaMethod('pointer', []); const className = spec.name; const interfaces = (spec.implements || []); const superClass = (spec.superClass || this.use('java.lang.Object')); const dexFields = []; const dexMethods = []; const dexSpec = { name: makeJniObjectTypeName(className), sourceFileName: makeSourceFileName(className), superClass: makeJniObjectTypeName(superClass.$n), interfaces: interfaces.map(iface => makeJniObjectTypeName(iface.$n)), fields: dexFields, methods: dexMethods }; const allInterfaces = interfaces.slice(); interfaces.forEach(iface => { Array.prototype.slice.call(iface.class.getInterfaces()) .forEach(baseIface => { const baseIfaceName = this.cast(baseIface, Class).getCanonicalName(); allInterfaces.push(this.use(baseIfaceName)); }); }); const fields = spec.fields || {}; Object.getOwnPropertyNames(fields).forEach(name => { const fieldType = this._getType(fields[name]); dexFields.push([name, fieldType.name]); }); const baseMethods = {}; const pendingOverloads = {}; allInterfaces.forEach(iface => { const h = iface.$borrowClassHandle(env); tempHandles.push(h); const ifaceHandle = h.value; iface.$ownMembers .filter(name => { return iface[name].overloads !== undefined; }) .forEach(name => { const method = iface[name]; const overloads = method.overloads; const overloadIds = overloads.map(overload => makeOverloadId(name, overload.returnType, overload.argumentTypes)); baseMethods[name] = [method, overloadIds, ifaceHandle]; overloads.forEach((overload, index) => { const id = overloadIds[index]; pendingOverloads[id] = [overload, ifaceHandle]; }); }); }); const methods = spec.methods || {}; const methodNames = Object.keys(methods); const methodEntries = methodNames.reduce((result, name) => { const entry = methods[name]; const rawName = (name === '$init') ? '<init>' : name; if (entry instanceof Array) { result.push(...entry.map(e => [rawName, e])); } else { result.push([rawName, entry]); } return result; }, []); const implMethods = []; methodEntries.forEach(([name, methodValue]) => { let type = INSTANCE_METHOD; let returnType; let argumentTypes; let thrownTypeNames = []; let impl; if (typeof methodValue === 'function') { const m = baseMethods[name]; if (m !== undefined && Array.isArray(m)) { const [baseMethod, overloadIds, parentTypeHandle] = m; if (overloadIds.length > 1) { throw new Error(`More than one overload matching '${name}': signature must be specified`); } delete pendingOverloads[overloadIds[0]]; const overload = baseMethod.overloads[0]; type = overload.type; returnType = overload.returnType; argumentTypes = overload.argumentTypes; impl = methodValue; const reflectedMethod = env.toReflectedMethod(parentTypeHandle, overload.handle, 0); const thrownTypes = invokeObjectMethodNoArgs(env.handle, reflectedMethod, Method.getGenericExceptionTypes); thrownTypeNames = readTypeNames(env, thrownTypes).map(makeJniObjectTypeName); env.deleteLocalRef(thrownTypes); env.deleteLocalRef(reflectedMethod); } else { returnType = this._getType('void'); argumentTypes = []; impl = methodValue; } } else { if (methodValue.isStatic) { type = STATIC_METHOD; } returnType = this._getType(methodValue.returnType || 'void'); argumentTypes = (methodValue.argumentTypes || []).map(name => this._getType(name)); impl = methodValue.implementation; if (typeof impl !== 'function') { throw new Error('Expected a function implementation for method: ' + name); } const id = makeOverloadId(name, returnType, argumentTypes); const pendingOverload = pendingOverloads[id]; if (pendingOverload !== undefined) { const [overload, parentTypeHandle] = pendingOverload; delete pendingOverloads[id]; type = overload.type; returnType = overload.returnType; argumentTypes = overload.argumentTypes; const reflectedMethod = env.toReflectedMethod(parentTypeHandle, overload.handle, 0); const thrownTypes = invokeObjectMethodNoArgs(env.handle, reflectedMethod, Method.getGenericExceptionTypes); thrownTypeNames = readTypeNames(env, thrownTypes).map(makeJniObjectTypeName); env.deleteLocalRef(thrownTypes); env.deleteLocalRef(reflectedMethod); } } const returnTypeName = returnType.name; const argumentTypeNames = argumentTypes.map(t => t.name); const signature = '(' + argumentTypeNames.join('') + ')' + returnTypeName; dexMethods.push([name, returnTypeName, argumentTypeNames, thrownTypeNames, (type === STATIC_METHOD) ? kAccStatic : 0]); implMethods.push([name, signature, type, returnType, argumentTypes, impl]); }); const unimplementedMethodIds = Object.keys(pendingOverloads); if (unimplementedMethodIds.length > 0) { throw new Error('Missing implementation for: ' + unimplementedMethodIds.join(', ')); } const dex = DexFile.fromBuffer(mkdex(dexSpec), this); try { dex.load(); } finally { dex.file.delete(); } const classWrapper = this.use(spec.name); const numMethods = methodEntries.length; if (numMethods > 0) { const methodElementSize = 3 * pointerSize; const methodElements = Memory.alloc(numMethods * methodElementSize); const nativeMethods = []; const temporaryHandles = []; implMethods.forEach(([name, signature, type, returnType, argumentTypes, impl], index) => { const rawName = Memory.allocUtf8String(name); const rawSignature = Memory.allocUtf8String(signature); const rawImpl = implement(name, classWrapper, type, returnType, argumentTypes, impl); methodElements.add(index * methodElementSize).writePointer(rawName); methodElements.add((index * methodElementSize) + pointerSize).writePointer(rawSignature); methodElements.add((index * methodElementSize) + (2 * pointerSize)).writePointer(rawImpl); temporaryHandles.push(rawName, rawSignature); nativeMethods.push(rawImpl); }); const h = classWrapper.$borrowClassHandle(env); tempHandles.push(h); const classHandle = h.value; env.registerNatives(classHandle, methodElements, numMethods); env.throwIfExceptionPending(); classWrapper.$nativeMethods = nativeMethods; } return classWrapper; } finally { tempHandles.forEach(h => { h.unref(env); }); } } choose (specifier, callbacks) { const env = vm.getEnv(); const { flavor } = api; if (flavor === 'jvm') { this._chooseObjectsJvm(specifier, env, callbacks); } else if (flavor === 'art') { const legacyApiMissing = api['art::gc::Heap::VisitObjects'] === undefined; if (legacyApiMissing) { const preA12ApiMissing = api['art::gc::Heap::GetInstances'] === undefined; if (preA12ApiMissing) { return this._chooseObjectsJvm(specifier, env, callbacks); } } android.withRunnableArtThread(vm, env, thread => { if (legacyApiMissing) { this._chooseObjectsArtPreA12(specifier, env, thread, callbacks); } else { this._chooseObjectsArtLegacy(specifier, env, thread, callbacks); } }); } else { this._chooseObjectsDalvik(specifier, env, callbacks); } } _chooseObjectsJvm (className, env, callbacks) { const classWrapper = this.use(className); const { jvmti } = api; const JVMTI_ITERATION_CONTINUE = 1; const JVMTI_HEAP_OBJECT_EITHER = 3; const h = classWrapper.$borrowClassHandle(env); const tag = int64(h.value.toString()); try { const heapObjectCallback = new NativeCallback((classTag, size, tagPtr, userData) => { tagPtr.writeS64(tag); return JVMTI_ITERATION_CONTINUE; }, 'int', ['int64', 'int64', 'pointer', 'pointer']); jvmti.iterateOverInstancesOfClass(h.value, JVMTI_HEAP_OBJECT_EITHER, heapObjectCallback, h.value); const tagPtr = Memory.alloc(8); tagPtr.writeS64(tag); const countPtr = Memory.alloc(jsizeSize); const objectsPtr = Memory.alloc(pointerSize); jvmti.getObjectsWithTags(1, tagPtr, countPtr, objectsPtr, NULL); const count = countPtr.readS32(); const objects = objectsPtr.readPointer(); const handles = []; for (let i = 0; i !== count; i++) { handles.push(objects.add(i * pointerSize).readPointer()); } jvmti.deallocate(objects); try { for (const handle of handles) { const instance = this.cast(handle, classWrapper); const result = callbacks.onMatch(instance); if (result === 'stop') { break; } } callbacks.onComplete(); } finally { handles.forEach(handle => { env.deleteLocalRef(handle); }); } } finally { h.unref(env); } } _chooseObjectsArtPreA12 (className, env, thread, callbacks) { const classWrapper = this.use(className); const scope = android.VariableSizedHandleScope.$new(thread, vm); let needle; const h = classWrapper.$borrowClassHandle(env); try { const object = api['art::JavaVMExt::DecodeGlobal'](api.vm, thread, h.value); needle = scope.newHandle(object); } finally { h.unref(env); } const maxCount = 0; const instances = android.HandleVector.$new(); api['art::gc::Heap::GetInstances'](api.artHeap, scope, needle, maxCount, instances); const instanceHandles = instances.handles.map(handle => env.newGlobalRef(handle)); instances.$delete(); scope.$delete(); try { for (const handle of instanceHandles) { const instance = this.cast(handle, classWrapper); const result = callbacks.onMatch(instance); if (result === 'stop') { break; } } callbacks.onComplete(); } finally { instanceHandles.forEach(handle => { env.deleteGlobalRef(handle); }); } } _chooseObjectsArtLegacy (className, env, thread, callbacks) { const classWrapper = this.use(className); const instanceHandles = []; const addGlobalReference = api['art::JavaVMExt::AddGlobalRef']; const vmHandle = api.vm; let needle; const h = classWrapper.$borrowClassHandle(env); try { needle = api['art::JavaVMExt::DecodeGlobal'](vmHandle, thread, h.value).toInt32(); } finally { h.unref(env); } const collectMatchingInstanceHandles = android.makeObjectVisitorPredicate(needle, object => { instanceHandles.push(addGlobalReference(vmHandle, thread, object)); }); api['art::gc::Heap::VisitObjects'](api.artHeap, collectMatchingInstanceHandles, NULL); try { for (const handle of instanceHandles) { const instance = this.cast(handle, classWrapper); const result = callbacks.onMatch(instance); if (result === 'stop') { break; } } } finally { instanceHandles.forEach(handle => { env.deleteGlobalRef(handle); }); } callbacks.onComplete(); } _chooseObjectsDalvik (className, callerEnv, callbacks) { const classWrapper = this.use(className); if (api.addLocalReference === null) { const libdvm = Process.getModuleByName('libdvm.so'); let pattern; switch (Process.arch) { case 'arm': // Verified with 4.3.1 and 4.4.4 pattern = '2d e9 f0 41 05 46 15 4e 0c 46 7e 44 11 b3 43 68'; break; case 'ia32': // Verified with 4.3.1 and 4.4.2 pattern = '8d 64 24 d4 89 5c 24 1c 89 74 24 20 e8 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 85 d2'; break; } Memory.scan(libdvm.base, libdvm.size, pattern, { onMatch: (address, size) => { let wrapper; if (Process.arch === 'arm') { address = address.or(1); // Thumb wrapper = new NativeFunction(address, 'pointer', ['pointer', 'pointer']); } else { const thunk = Memory.alloc(Process.pageSize); Memory.patchCode(thunk, 16, code => { const cw = new X86Writer(code, { pc: thunk }); cw.putMovRegRegOffsetPtr('eax', 'esp', 4); cw.putMovRegRegOffsetPtr('edx', 'esp', 8); cw.putJmpAddress(address); cw.flush(); }); wrapper = new NativeFunction(thunk, 'pointer', ['pointer', 'pointer']); wrapper._thunk = thunk; } api.addLocalReference = wrapper; vm.perform(env => { enumerateInstances(this, env); }); return 'stop'; }, onError (reason) {}, onComplete () { if (api.addLocalReference === null) { callbacks.onComplete(); } } }); } else { enumerateInstances(this, callerEnv); } function enumerateInstances (factory, env) { const { DVM_JNI_ENV_OFFSET_SELF } = android; const thread = env.handle.add(DVM_JNI_ENV_OFFSET_SELF).readPointer(); let ptrClassObject; const h = classWrapper.$borrowClassHandle(env); try { ptrClassObject = api.dvmDecodeIndirectRef(thread, h.value); } finally { h.unref(env); } const pattern = ptrClassObject.toMatchPattern(); const heapSourceBase = api.dvmHeapSourceGetBase(); const heapSourceLimit = api.dvmHeapSourceGetLimit(); const size = heapSourceLimit.sub(heapSourceBase).toInt32(); Memory.scan(heapSourceBase, size, pattern, { onMatch: (address, size) => { if (api.dvmIsValidObject(address)) { vm.perform(env => { const thread = env.handle.add(DVM_JNI_ENV_OFFSET_SELF).readPointer(); let instance; const localReference = api.addLocalReference(thread, address); try { instance = factory.cast(localReference, classWrapper); } finally { env.deleteLocalRef(localReference); } const result = callbacks.onMatch(instance); if (result === 'stop') { return 'stop'; } }); } }, onError (reason) {}, onComplete () { callbacks.onComplete(); } }); } } openClassFile (filePath) { return new DexFile(filePath, null, this); } _getType (typeName, unbox = true) { return getType(typeName, unbox, this); } } function makeClassWrapperConstructor () { return function (handle, strategy, env, owned) { return Wrapper.call(this, handle, strategy, env, owned); }; } function Wrapper (handle, strategy, env, owned = true) { if (handle !== null) { if (owned) { const h = env.newGlobalRef(handle); this.$h = h; this.$r = Script.bindWeak(this, vm.makeHandleDestructor(h)); } else { this.$h = handle; this.$r = null; } } else { this.$h = null; this.$r = null; } this.$t = strategy; return new Proxy(this, wrapperHandler); } wrapperHandler = { has (target, property) { if (property in target) { return true; } return target.$has(property); }, get (target, property, receiver) { if (typeof property !== 'string' || property.startsWith('$') || property === 'class') { return target[property]; } const unwrap = target.$find(property); if (unwrap !== null) { return unwrap(receiver); } return target[property]; }, set (target, property, value, receiver) { target[property] = value; return true; }, ownKeys (target) { return target.$list(); }, getOwnPropertyDescriptor (target, property) { if (Object.prototype.hasOwnProperty.call(target, property)) { return Object.getOwnPropertyDescriptor(target, property); } return { writable: false, configurable: true, enumerable: true }; } }; Object.defineProperties(Wrapper.prototype, { [Symbol.for('new')]: { enumerable: false, get () { return this.$getCtor('allocAndInit'); } }, $new: { enumerable: true, get () { return this[Symbol.for('new')]; } }, [Symbol.for('alloc')]: { enumerable: false, value () { const env = vm.getEnv(); const h = this.$borrowClassHandle(env); try { const obj = env.allocObject(h.value); const factory = this.$f; return factory.cast(obj, this); } finally { h.unref(env); } } }, $alloc: { enumerable: true, get () { return this[Symbol.for('alloc')]; } }, [Symbol.for('init')]: { enumerable: false, get () { return this.$getCtor('initOnly'); } }, $init: { enumerable: true, get () { return this[Symbol.for('init')]; } }, [Symbol.for('dispose')]: { enumerable: false, value () { const ref = this.$r; if (ref !== null) { this.$r = null; Script.unbindWeak(ref); } if (this.$h !== null) { this.$h = undefined; } } }, $dispose: { enumerable: true, get () { return this[Symbol.for('dispose')]; } }, [Symbol.for('clone')]: { enumerable: false, value (env) { const C = this.$C; return new C(this.$h, this.$t, env); } }, $clone: { value (env) { return this[Symbol.for('clone')](env); } }, [Symbol.for('class')]: { enumerable: false, get () { const env = vm.getEnv(); const h = this.$borrowClassHandle(env); try { const factory = this.$f; return factory.cast(h.value, factory.use('java.lang.Class')); } finally { h.unref(env); } } }, class: { enumerable: true, get () { return this[Symbol.for('class')]; } }, [Symbol.for('className')]: { enumerable: false, get () { const handle = this.$h; if (handle === null) { return this.$n; } return vm.getEnv().getObjectClassName(handle); } }, $className: { enumerable: true, get () { return this[Symbol.for('className')]; } }, [Symbol.for('ownMembers')]: { enumerable: false, get () { const model = this.$l; return model.list(); } }, $ownMembers: { enumerable: true, get () { return this[Symbol.for('ownMembers')]; } }, [Symbol.for('super')]: { enumerable: false, get () { const env = vm.getEnv(); const C = this.$s.$C; return new C(this.$h, STRATEGY_DIRECT, env); } }, $super: { enumerable: true, get () { return this[Symbol.for('super')]; } }, [Symbol.for('s')]: { enumerable: false, get () { const proto = Object.getPrototypeOf(this); let superWrapper = proto.$_s; if (superWrapper === undefined) { const env = vm.getEnv(); const h = this.$borrowClassHandle(env); try { const superHandle = env.getSuperclass(h.value); if (!superHandle.isNull()) { try { const superClassName = env.getClassName(superHandle); const factory = proto.$f; superWrapper = factory._getUsedClass(superClassName); if (superWrapper === undefined) { try { const getSuperClassHandle = makeSuperHandleGetter(this); superWrapper = factory._make(superClassName, getSuperClassHandle, env); } finally { factory._setUsedClass(superClassName, superWrapper); } } } finally { env.deleteLocalRef(superHandle); } } else { superWrapper = null; } } finally { h.unref(env); } proto.$_s = superWrapper; } return superWrapper; } }, $s: { get () { return this[Symbol.for('s')]; } }, [Symbol.for('isSameObject')]: { enumerable: false, value (obj) { const env = vm.getEnv(); return env.isSameObject(obj.$h, this.$h); } }, $isSameObject: { value (obj) { return this[Symbol.for('isSameObject')](obj); } }, [Symbol.for('getCtor')]: { enumerable: false, value (type) { const slot = this.$c; let ctor = slot[0]; if (ctor === null) { const env = vm.getEnv(); const h = this.$borrowClassHandle(env); try { ctor = makeConstructor(h.value, this.$w, env); slot[0] = ctor; } finally { h.unref(env); } } return ctor[type]; } }, $getCtor: { value (type) { return this[Symbol.for('getCtor')](type); } }, [Symbol.for('borrowClassHandle')]: { enumerable: false, value (env) { const className = this.$n; const classHandles = this.$f._classHandles; let handle = classHandles.get(className); if (handle === undefined) { handle = new ClassHandle(this.$gch(env), env); classHandles.set(className, handle, env); } return handle.ref(); } }, $borrowClassHandle: { value (env) { return this[Symbol.for('borrowClassHandle')](env); } }, [Symbol.for('copyClassHandle')]: { enumerable: false, value (env) { const h = this.$borrowClassHandle(env); try { return env.newLocalRef(h.value); } finally { h.unref(env); } } }, $copyClassHandle: { value (env) { return this[Symbol.for('copyClassHandle')](env); } }, [Symbol.for('getHandle')]: { enumerable: false, value (env) { const handle = this.$h; const isDisposed = handle === undefined; if (isDisposed) { throw new Error('Wrapper is disposed; perhaps it was borrowed from a hook ' + 'instead of calling Java.retain() to make a long-lived wrapper?'); } return handle; } }, $getHandle: { value (env) { return this[Symbol.for('getHandle')](env); } }, [Symbol.for('list')]: { enumerable: false, value () { const superWrapper = this.$s; const superMembers = (superWrapper !== null) ? superWrapper.$list() : []; const model = this.$l; return Array.from(new Set(superMembers.concat(model.list()))); } }, $list: { get () { return this[Symbol.for('list')]; } }, [Symbol.for('has')]: { enumerable: false, value (member) { const members = this.$m; if (members.has(member)) { return true; } const model = this.$l; if (model.has(member)) { return true; } const superWrapper = this.$s; if (superWrapper !== null && superWrapper.$has(member)) { return true; } return false; } }, $has: { value (member) { return this[Symbol.for('has')](member); } }, [Symbol.for('find')]: { enumerable: false, value (member) { const members = this.$m; let value = members.get(member); if (value !== undefined) { return value; } const model = this.$l; const spec = model.find(member); if (spec !== null) { const env = vm.getEnv(); const h = this.$borrowClassHandle(env); try { value = makeMember(member, spec, h.value, this.$w, env); } finally { h.unref(env); } members.set(member, value); return value; } const superWrapper = this.$s; if (superWrapper !== null) { return superWrapper.$find(member); } return null; } }, $find: { value (member) { return this[Symbol.for('find')](member); } }, [Symbol.for('toJSON')]: { enumerable: false, value () { const wrapperName = this.$n; const handle = this.$h; if (handle === null) { return `<class: ${wrapperName}>`; } const actualName = this.$className; if (wrapperName === actualName) { return `<instance: ${wrapperName}>`; } return `<instance: ${wrapperName}, $className: ${actualName}>`; } }, toJSON: { get () { return this[Symbol.for('toJSON')]; } } }); function ClassHandle (value, env) { this.value = env.newGlobalRef(value); env.deleteLocalRef(value); this.refs = 1; } ClassHandle.prototype.ref = function () { this.refs++; return this; }; ClassHandle.prototype.unref = function (env) { if (--this.refs === 0) { env.deleteGlobalRef(this.value); } }; function releaseClassHandle (handle, env) { handle.unref(env); } function makeBasicClassHandleGetter (className) { const canonicalClassName = className.replace(/\./g, '/'); return function (env) { const tid = getCurrentThreadId(); ignore(tid); try { return env.findClass(canonicalClassName); } finally { unignore(tid); } }; } function makeLoaderClassHandleGetter (className, usedLoader, callerEnv) { if (cachedLoaderMethod === null) { cachedLoaderInvoke = callerEnv.vaMethod('pointer', ['pointer']); cachedLoaderMethod = usedLoader.loadClass.overload('java.lang.String').handle; } callerEnv = null; return function (env) { const classNameValue = env.newStringUtf(className); const tid = getCurrentThreadId(); ignore(tid); try { const result = cachedLoaderInvoke(env.handle, usedLoader.$h, cachedLoaderMethod, classNameValue); env.throwIfExceptionPending(); return result; } finally { unignore(tid); env.deleteLocalRef(classNameValue); } }; } function makeSuperHandleGetter (classWrapper) { return function (env) { const h = classWrapper.$borrowClassHandle(env); try { return env.getSuperclass(h.value); } finally { h.unref(env); } }; } function makeConstructor (classHandle, classWrapper, env) { const { $n: className, $f: factory } = classWrapper; const methodName = basename(className); const Class = env.javaLangClass(); const Constructor = env.javaLangReflectConstructor(); const invokeObjectMethodNoArgs = env.vaMethod('pointer', []); const invokeUInt8MethodNoArgs = env.vaMethod('uint8', []); const jsCtorMethods = []; const jsInitMethods = []; const jsRetType = factory._getType(className, false); const jsVoidType = factory._getType('void', false); const constructors = invokeObjectMethodNoArgs(env.handle, classHandle, Class.getDeclaredConstructors); try { const n = env.getArrayLength(constructors); if (n !== 0) { for (let i = 0; i !== n; i++) { let methodId, types; const constructor = env.getObjectArrayElement(constructors, i); try { methodId = env.fromReflectedMethod(constructor); types = invokeObjectMethodNoArgs(env.handle, constructor, Constructor.getGenericParameterTypes); } finally { env.deleteLocalRef(constructor); } let jsArgTypes; try { jsArgTypes = readTypeNames(env, types).map(name => factory._getType(name)); } finally { env.deleteLocalRef(types); } jsCtorMethods.push(makeMethod(methodName, classWrapper, CONSTRUCTOR_METHOD, methodId, jsRetType, jsArgTypes, env)); jsInitMethods.push(makeMethod(methodName, classWrapper, INSTANCE_METHOD, methodId, jsVoidType, jsArgTypes, env)); } } else { const isInterface = invokeUInt8MethodNoArgs(env.handle, classHandle, Class.isInterface); if (isInterface) { throw new Error('cannot instantiate an interface'); } const defaultClass = env.javaLangObject(); const defaultConstructor = env.getMethodId(defaultClass, '<init>', '()V'); jsCtorMethods.push(makeMethod(methodName, classWrapper, CONSTRUCTOR_METHOD, defaultConstructor, jsRetType, [], env)); jsInitMethods.push(makeMethod(methodName, classWrapper, INSTANCE_METHOD, defaultConstructor, jsVoidType, [], env)); } } finally { env.deleteLocalRef(constructors); } if (jsInitMethods.length === 0) { throw new Error('no supported overloads'); } return { allocAndInit: makeMethodDispatcher(jsCtorMethods), initOnly: makeMethodDispatcher(jsInitMethods) }; } function makeMember (name, spec, classHandle, classWrapper, env) { if (spec.startsWith('m')) { return makeMethodFromSpec(name, spec, classHandle, classWrapper, env); } return makeFieldFromSpec(name, spec, classHandle, classWrapper, env); } function makeMethodFromSpec (name, spec, classHandle, classWrapper, env) { const { $f: factory } = classWrapper; const overloads = spec.split(':').slice(1); const Method = env.javaLangReflectMethod(); const invokeObjectMethodNoArgs = env.vaMethod('pointer', []); const invokeUInt8MethodNoArgs = env.vaMethod('uint8', []); const methods = overloads.map(params => { const type = (params[0] === 's') ? STATIC_METHOD : INSTANCE_METHOD; const methodId = ptr(params.substr(1)); let jsRetType; const jsArgTypes = []; const handle = env.toReflectedMethod(classHandle, methodId, (type === STATIC_METHOD) ? 1 : 0); try { const isVarArgs = !!invokeUInt8MethodNoArgs(env.handle, handle, Method.isVarArgs); const retType = invokeObjectMethodNoArgs(env.handle, handle, Method.getGenericReturnType); env.throwIfExceptionPending(); try { jsRetType = factory._getType(env.getTypeName(retType)); } finally { env.deleteLocalRef(retType); } const argTypes = invokeObjectMethodNoArgs(env.handle, handle, Method.getParameterTypes); try { const n = env.getArrayLength(argTypes); for (let i = 0; i !== n; i++) { const t = env.getObjectArrayElement(argTypes, i); let argClassName; try { argClassName = (isVarArgs && i === n - 1) ? env.getArrayTypeName(t) : env.getTypeName(t); } finally { env.deleteLocalRef(t); } const argType = factory._getType(argClassName); jsArgTypes.push(argType); } } finally { env.deleteLocalRef(argTypes); } } catch (e) { return null; } finally { env.deleteLocalRef(handle); } return makeMethod(name, classWrapper, type, methodId, jsRetType, jsArgTypes, env); }) .filter(m => m !== null); if (methods.length === 0) { throw new Error('No supported overloads'); } if (name === 'valueOf') { ensureDefaultValueOfImplemented(methods); } const result = makeMethodDispatcher(methods); return function (receiver) { return result; }; } function makeMethodDispatcher (overloads) { const m = makeMethodDispatcherCallable(); Object.setPrototypeOf(m, dispatcherPrototype); m._o = overloads; return m; } function makeMethodDispatcherCallable () { const m = function () { return m.invoke(this, arguments); }; return m; } dispatcherPrototype = Object.create(Function.prototype, { overloads: { enumerable: true, get () { return this._o; } }, overload: { value (...args) { const overloads = this._o; const numArgs = args.length; const signature = args.join(':'); for (let i = 0; i !== overloads.length; i++) { const method = overloads[i]; const { argumentTypes } = method; if (argumentTypes.length !== numArgs) { continue; } const s = argumentTypes.map(t => t.className).join(':'); if (s === signature) { return method; } } throwOverloadError(this.methodName, this.overloads, 'specified argument types do not match any of:'); } }, methodName: { enumerable: true, get () { return this._o[0].methodName; } }, holder: { enumerable: true, get () { return this._o[0].holder; } }, type: { enumerable: true, get () { return this._o[0].type; } }, handle: { enumerable: true, get () { throwIfDispatcherAmbiguous(this); return this._o[0].handle; } }, implementation: { enumerable: true, get () { throwIfDispatcherAmbiguous(this); return this._o[0].implementation; }, set (fn) { throwIfDispatcherAmbiguous(this); this._o[0].implementation = fn; } }, returnType: { enumerable: true, get () { throwIfDispatcherAmbiguous(this); return this._o[0].returnType; } }, argumentTypes: { enumerable: true, get () { throwIfDispatcherAmbiguous(this); return this._o[0].argumentTypes; } }, canInvokeWith: { enumerable: true, get (args) { throwIfDispatcherAmbiguous(this); return this._o[0].canInvokeWith; } }, clone: { enumerable: true, value (options) { throwIfDispatcherAmbiguous(this); return this._o[0].clone(options); } }, invoke: { value (receiver, args) { const overloads = this._o; const isInstance = receiver.$h !== null; for (let i = 0; i !== overloads.length; i++) { const method = overloads[i]; if (!method.canInvokeWith(args)) { continue; } if (method.type === INSTANCE_METHOD && !isInstance) { const name = this.methodName; if (name === 'toString') { return `<class: ${receiver.$n}>`; } throw new Error(name + ': cannot call instance method without an instance'); } return method.apply(receiver, args); } if (this.methodName === 'toString') { return `<class: ${receiver.$n}>`; } throwOverloadError(this.methodName, this.overloads, 'argument types do not match any of:'); } } }); function makeOverloadId (name, returnType, argumentTypes) { return `${returnType.className} ${name}(${argumentTypes.map(t => t.className).join(', ')})`; } function throwIfDispatcherAmbiguous (dispatcher) { const methods = dispatcher._o; if (methods.length > 1) { throwOverloadError(methods[0].methodName, methods, 'has more than one overload, use .overload(<signature>) to choose from:'); } } function throwOverloadError (name, methods, message) { const methodsSortedByArity = methods.slice().sort((a, b) => a.argumentTypes.length - b.argumentTypes.length); const overloads = methodsSortedByArity.map(m => { const argTypes = m.argumentTypes; if (argTypes.length > 0) { return '.overload(\'' + m.argumentTypes.map(t => t.className).join('\', \'') + '\')'; } else { return '.overload()'; } }); throw new Error(`${name}(): ${message}\n\t${overloads.join('\n\t')}`); } function makeMethod (methodName, classWrapper, type, methodId, retType, argTypes, env, invocationOptions) { const rawRetType = retType.type; const rawArgTypes = argTypes.map((t) => t.type); if (env === null) { env = vm.getEnv(); } let callVirtually, callDirectly; if (type === INSTANCE_METHOD) { callVirtually = env.vaMethod(rawRetType, rawArgTypes, invocationOptions); callDirectly = env.nonvirtualVaMethod(rawRetType, rawArgTypes, invocationOptions); } else if (type === STATIC_METHOD) { callVirtually = env.staticVaMethod(rawRetType, rawArgTypes, invocationOptions); callDirectly = callVirtually; } else { callVirtually = env.constructor(rawArgTypes, invocationOptions); callDirectly = callVirtually; } return makeMethodInstance([methodName, classWrapper, type, methodId, retType, argTypes, callVirtually, callDirectly]); } function makeMethodInstance (params) { const m = makeMethodCallable(); Object.setPrototypeOf(m, methodPrototype); m._p = params; return m; } function makeMethodCallable () { const m = function () { return m.invoke(this, arguments); }; return m; } methodPrototype = Object.create(Function.prototype, { methodName: { enumerable: true, get () { return this._p[0]; } }, holder: { enumerable: true, get () { return this._p[1]; } }, type: { enumerable: true, get () { return this._p[2]; } }, handle: { enumerable: true, get () { return this._p[3]; } }, implementation: { enumerable: true, get () { const replacement = this._r; return (replacement !== undefined) ? replacement : null; }, set (fn) { const params = this._p; const holder = params[1]; const type = params[2]; if (type === CONSTRUCTOR_METHOD) { throw new Error('Reimplementing $new is not possible; replace implementation of $init instead'); } const existingReplacement = this._r; if (existingReplacement !== undefined) { holder.$f._patchedMethods.delete(this); const mangler = existingReplacement._m; mangler.revert(vm); this._r = undefined; } if (fn !== null) { const [methodName, classWrapper, type, methodId, retType, argTypes] = params; const replacement = implement(methodName, classWrapper, type, retType, argTypes, fn, this); const mangler = makeMethodMangler(methodId); replacement._m = mangler; this._r = replacement; mangler.replace(replacement, type === INSTANCE_METHOD, argTypes, vm, api); holder.$f._patchedMethods.add(this); } } }, returnType: { enumerable: true, get () { return this._p[4]; } }, argumentTypes: { enumerable: true, get () { return this._p[5]; } }, canInvokeWith: { enumerable: true, value (args) { const argTypes = this._p[5]; if (args.length !== argTypes.length) { return false; } return argTypes.every((t, i) => { return t.isCompatible(args[i]); }); } }, clone: { enumerable: true, value (options) { const params = this._p.slice(0, 6); return makeMethod(...params, null, options); } }, invoke: { value (receiver, args) { const env = vm.getEnv(); const params = this._p; const type = params[2]; const retType = params[4]; const argTypes = params[5]; const replacement = this._r; const isInstanceMethod = type === INSTANCE_METHOD; const numArgs = args.length; const frameCapacity = 2 + numArgs; env.pushLocalFrame(frameCapacity); let borrowedHandle = null; try { let jniThis; if (isInstanceMethod) { jniThis = receiver.$getHandle(); } else { borrowedHandle = receiver.$borrowClassHandle(env); jniThis = borrowedHandle.value; } let methodId; let strategy = receiver.$t; if (replacement === undefined) { methodId = params[3]; } else { const mangler = replacement._m; methodId = mangler.resolveTarget(receiver, isInstanceMethod, env, api); if (isArtVm) { const pendingCalls = replacement._c; if (pendingCalls.has(getCurrentThreadId())) { strategy = STRATEGY_DIRECT; } } } const jniArgs = [ env.handle, jniThis, methodId ]; for (let i = 0; i !== numArgs; i++) { jniArgs.push(argTypes[i].toJni(args[i], env)); } let jniCall; if (strategy === STRATEGY_VIRTUAL) { jniCall = params[6]; } else { jniCall = params[7]; if (isInstanceMethod) { jniArgs.splice(2, 0, receiver.$copyClassHandle(env)); } } const jniRetval = jniCall.apply(null, jniArgs); env.throwIfExceptionPending(); return retType.fromJni(jniRetval, env, true); } finally { if (borrowedHandle !== null) { borrowedHandle.unref(env); } env.popLocalFrame(NULL); } } }, toString: { enumerable: true, value () { return `function ${this.methodName}(${this.argumentTypes.map(t => t.className).join(', ')}): ${this.returnType.className}`; } } }); function implement (methodName, classWrapper, type, retType, argTypes, handler, fallback = null) { const pendingCalls = new Set(); const f = makeMethodImplementation([methodName, classWrapper, type, retType, argTypes, handler, fallback, pendingCalls]); const impl = new NativeCallback(f, retType.type, ['pointer', 'pointer'].concat(argTypes.map(t => t.type))); impl._c = pendingCalls; return impl; } function makeMethodImplementation (params) { return function () { return handleMethodInvocation(arguments, params); }; } function handleMethodInvocation (jniArgs, params) { const env = new Env(jniArgs[0], vm); const [methodName, classWrapper, type, retType, argTypes, handler, fallback, pendingCalls] = params; const ownedObjects = []; let self; if (type === INSTANCE_METHOD) { const C = classWrapper.$C; self = new C(jniArgs[1], STRATEGY_VIRTUAL, env, false); } else { self = classWrapper; } const tid = getCurrentThreadId(); env.pushLocalFrame(3); let haveFrame = true; vm.link(tid, env);