• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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