1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.Q; 4 5 import com.google.common.base.Preconditions; 6 import dalvik.system.VMRuntime; 7 import java.lang.ref.WeakReference; 8 import java.lang.reflect.Array; 9 import java.lang.reflect.Method; 10 import java.nio.ByteBuffer; 11 import java.nio.ByteOrder; 12 import java.util.Collections; 13 import java.util.HashMap; 14 import java.util.Map; 15 import java.util.WeakHashMap; 16 import java.util.concurrent.atomic.AtomicLong; 17 import javax.annotation.Nullable; 18 import org.robolectric.annotation.Implementation; 19 import org.robolectric.annotation.Implements; 20 import org.robolectric.annotation.Resetter; 21 22 @Implements(value = VMRuntime.class, isInAndroidSdk = false) 23 public class ShadowVMRuntime { 24 25 /** 26 * A map of allocated non movable arrays to the (Direct)ByteBuffer backing it 27 * 28 * <p>The JVM does not directly support newNonMovableArray. So as a workaround, this class will 29 * allocate a direct ByteBuffer for use in native code. It is the responsibility the shadow code 30 * to update any associated buffers with the data from native code. 31 */ 32 private final Map<Object, ByteBuffer> realNonMovableArrays = 33 Collections.synchronizedMap(new WeakHashMap<>()); 34 35 private final Map<Long, WeakReference<Object>> nonMovableArraysReverse = 36 Collections.synchronizedMap(new HashMap<>()); 37 38 /** 39 * Currently, {@link android.content.res.TypedArray} uses newNonMovableArray, but does not need to 40 * access the data from native code. So in this case we will allocate a fake pointer. 41 */ 42 private final Map<Object, Long> fakeNonMovableArrays = 43 Collections.synchronizedMap(new WeakHashMap<>()); 44 45 private final AtomicLong nextFakeArrayPointer = new AtomicLong(); 46 47 // This is a hack to get the address of a DirectByteBuffer. The Method object is cached to reduce 48 // the overhead of reflection. This method is invoked extensively during layout inflation. This 49 // reflection requires the `--add-opens=java.base/java.nio=ALL-UNNAMED` JVM flag. This value is 50 // lazy so tests can avoid having to add the flag where it is not needed. 51 private static Method addressMethod; 52 53 // There actually isn't any android JNI code to call through to in Robolectric due to 54 // cross-platform compatibility issues. We default to a reasonable value that reflects the devices 55 // that would commonly run this code. 56 private static boolean is64Bit = true; 57 58 @Nullable private static String currentInstructionSet = null; 59 60 @Implementation newUnpaddedArray(Class<?> klass, int size)61 public Object newUnpaddedArray(Class<?> klass, int size) { 62 return Array.newInstance(klass, size); 63 } 64 65 @Implementation newNonMovableArray(Class<?> type, int size)66 public Object newNonMovableArray(Class<?> type, int size) { 67 Preconditions.checkArgument( 68 type == int.class || type == float.class || type == byte.class, 69 "unsupported type %s", 70 type.getName()); 71 Object arrayInstance = Array.newInstance(type, size); 72 if (type == float.class && size == 8) { 73 // This is being called from android.graphics.PathIterator, so we need to allocate a real 74 // ByteBuffer that can be accessed from native code. 75 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4 * size); 76 byteBuffer.order(ByteOrder.nativeOrder()); 77 realNonMovableArrays.put(arrayInstance, byteBuffer); 78 nonMovableArraysReverse.put( 79 getAddressOfDirectByteBuffer(byteBuffer), new WeakReference<>(arrayInstance)); 80 } else { 81 // This is being called from android.content.res.TypedArray, so we need to allocate a fake 82 // pointer. 83 long fakePointer = nextFakeArrayPointer.incrementAndGet(); 84 fakeNonMovableArrays.put(arrayInstance, fakePointer); 85 nonMovableArraysReverse.put(fakePointer, new WeakReference<>(arrayInstance)); 86 } 87 return arrayInstance; 88 } 89 90 /** Returns a unique identifier of the object instead of a 'native' address. */ 91 @Implementation addressOf(Object obj)92 public long addressOf(Object obj) { 93 if (obj == null) { 94 return 0; 95 } 96 Preconditions.checkArgument( 97 obj.getClass().isArray(), "addressOf(Object) is only supported for array objects"); 98 Class<?> arrayClass = obj.getClass().getComponentType(); 99 Preconditions.checkArgument( 100 arrayClass.isPrimitive(), 101 "addressOf(Object) is only supported for primitive array objects"); 102 if (arrayClass == float.class && Array.getLength(obj) == 8) { 103 // This is being called from android.graphics.PathIterator. 104 ByteBuffer byteBuffer = realNonMovableArrays.get(obj); 105 if (byteBuffer == null) { 106 throw new IllegalArgumentException("Trying to get address of unknown object"); 107 } 108 return getAddressOfDirectByteBuffer(byteBuffer); 109 } else { 110 // This is being called from android.content.res.TypedArray. 111 Long address = fakeNonMovableArrays.get(obj); 112 if (address == null) { 113 throw new IllegalArgumentException("Trying to get address of unknown object"); 114 } 115 return address; 116 } 117 } 118 getAddressOfDirectByteBuffer(ByteBuffer byteBuffer)119 private long getAddressOfDirectByteBuffer(ByteBuffer byteBuffer) { 120 synchronized (ShadowVMRuntime.class) { 121 if (addressMethod == null) { 122 try { 123 addressMethod = Class.forName("java.nio.DirectByteBuffer").getMethod("address"); 124 addressMethod.setAccessible(true); 125 } catch (ReflectiveOperationException e) { 126 throw new LinkageError("Error accessing address method", e); 127 } 128 } 129 } 130 131 try { 132 return (long) addressMethod.invoke(byteBuffer); 133 } catch (ReflectiveOperationException e) { 134 throw new LinkageError("Error invoking address method", e); 135 } 136 } 137 138 /** Returns the object previously registered with {@link #addressOf(Object)}. */ 139 @Nullable getObjectForAddress(long address)140 Object getObjectForAddress(long address) { 141 WeakReference<Object> weakReference = nonMovableArraysReverse.get(address); 142 if (weakReference == null) { 143 return null; 144 } 145 return weakReference.get(); 146 } 147 148 /** 149 * Returns whether the VM is running in 64-bit mode. Available in Android L+. Defaults to true. 150 */ 151 @Implementation is64Bit()152 protected boolean is64Bit() { 153 return ShadowVMRuntime.is64Bit; 154 } 155 156 /** Sets whether the VM is running in 64-bit mode. */ setIs64Bit(boolean is64Bit)157 public static void setIs64Bit(boolean is64Bit) { 158 ShadowVMRuntime.is64Bit = is64Bit; 159 } 160 161 /** Returns the instruction set of the current runtime. */ 162 @Implementation getCurrentInstructionSet()163 protected static String getCurrentInstructionSet() { 164 return currentInstructionSet; 165 } 166 167 /** Sets the instruction set of the current runtime. */ setCurrentInstructionSet(@ullable String currentInstructionSet)168 public static void setCurrentInstructionSet(@Nullable String currentInstructionSet) { 169 ShadowVMRuntime.currentInstructionSet = currentInstructionSet; 170 } 171 getBackingBuffer(long address)172 ByteBuffer getBackingBuffer(long address) { 173 Object array = getObjectForAddress(address); 174 if (array == null) { 175 return null; 176 } 177 return realNonMovableArrays.get(array); 178 } 179 180 @Resetter reset()181 public static void reset() { 182 ShadowVMRuntime.is64Bit = true; 183 ShadowVMRuntime.currentInstructionSet = null; 184 } 185 186 @Implementation(minSdk = Q) getNotifyNativeInterval()187 protected static int getNotifyNativeInterval() { 188 // The value '384' is from 189 // https://cs.android.com/android/platform/superproject/+/android-12.0.0_r18:art/runtime/gc/heap.h;l=172 190 // Note that value returned is irrelevant for the JVM, it just has to be greater than zero to 191 // avoid a divide-by-zero error in VMRuntime.notifyNativeAllocation. 192 return 384; // must be greater than 0 193 } 194 } 195