From 02571a9535ab29a60b19d5b695f131b2a7cd8717 Mon Sep 17 00:00:00 2001 From: Hendra Date: Thu, 27 Aug 2026 22:36:20 +0800 Subject: [PATCH] Suppress advisory MIDlet GC through universal transformer --- .../java/javax/microedition/lcdui/Canvas.java | 2 +- .../microedition/shell/GuestTimingBridge.java | 2 - .../javax/microedition/shell/MicroLoader.java | 6 +- .../microedition/shell/MidletSystem.java | 15 +++++ .../shell/timing/TimingTransformMetadata.java | 56 ----------------- .../transform/MidletTransformMetadata.java | 51 ++++++++++++++++ .../j2meloader/config/ConfigActivity.java | 4 +- .../woesss/j2me/installer/AppInstaller.java | 4 +- .../woesss/j2me/installer/AppReconverter.java | 8 +-- .../microedition/shell/MidletSystemTest.java | 30 +++++++++ .../timing/TimingTransformMetadataTest.java | 61 ------------------- .../MidletTransformMetadataTest.java | 60 ++++++++++++++++++ .../android/asm/AndroidMethodVisitor.java | 15 ++++- .../android/asm/AndroidMethodVisitorTest.java | 34 +++++++++++ .../android/asm/AndroidProducerDexTest.java | 20 +++++- 15 files changed, 235 insertions(+), 133 deletions(-) delete mode 100644 app/src/main/java/javax/microedition/shell/timing/TimingTransformMetadata.java create mode 100644 app/src/main/java/javax/microedition/shell/transform/MidletTransformMetadata.java create mode 100644 app/src/test/java/javax/microedition/shell/MidletSystemTest.java delete mode 100644 app/src/test/java/javax/microedition/shell/timing/TimingTransformMetadataTest.java create mode 100644 app/src/test/java/javax/microedition/shell/transform/MidletTransformMetadataTest.java diff --git a/app/src/main/java/javax/microedition/lcdui/Canvas.java b/app/src/main/java/javax/microedition/lcdui/Canvas.java index 1cd6a57cc..6347f1ce1 100644 --- a/app/src/main/java/javax/microedition/lcdui/Canvas.java +++ b/app/src/main/java/javax/microedition/lcdui/Canvas.java @@ -200,7 +200,7 @@ public static void setSettings(ProfileModel settings) { parallelRedraw = (mode == 0 || mode == 3) && settings.parallelRedrawScreen; } - /** Enables the diagnostic only when the loaded artifact advertises the timing transform ABI. */ + /** Enables the diagnostic only when the loaded artifact has the current universal transform. */ public static void setTimingOverlayEnabled(boolean enabled) { timingOverlayEnabled = enabled; } diff --git a/app/src/main/java/javax/microedition/shell/GuestTimingBridge.java b/app/src/main/java/javax/microedition/shell/GuestTimingBridge.java index fc931727f..5008cbe03 100644 --- a/app/src/main/java/javax/microedition/shell/GuestTimingBridge.java +++ b/app/src/main/java/javax/microedition/shell/GuestTimingBridge.java @@ -33,8 +33,6 @@ * provide or shadow this class. */ public final class GuestTimingBridge { - public static final int ABI_VERSION = 4; - private static final Object LOCK = new Object(); private static TimingSession activeSession; diff --git a/app/src/main/java/javax/microedition/shell/MicroLoader.java b/app/src/main/java/javax/microedition/shell/MicroLoader.java index a80683e7d..aa8821bfa 100644 --- a/app/src/main/java/javax/microedition/shell/MicroLoader.java +++ b/app/src/main/java/javax/microedition/shell/MicroLoader.java @@ -82,7 +82,7 @@ import javax.microedition.shell.timing.EmulationSpeed; import javax.microedition.shell.timing.TimingSession; import javax.microedition.shell.timing.TimingMode; -import javax.microedition.shell.timing.TimingTransformMetadata; +import javax.microedition.shell.transform.MidletTransformMetadata; public class MicroLoader { private static final String TAG = MicroLoader.class.getName(); @@ -200,9 +200,9 @@ private boolean hasCompatibleTimingTransform() { try { Descriptor descriptor = new Descriptor( new File(appDir, Config.MIDLET_MANIFEST_FILE), false); - return TimingTransformMetadata.isCompatible(descriptor.getAttrs()); + return MidletTransformMetadata.isCompatible(descriptor.getAttrs()); } catch (IOException | RuntimeException e) { - Log.w(TAG, "Timing transform metadata is unavailable", e); + Log.w(TAG, "MIDlet transform metadata is unavailable", e); return false; } } diff --git a/app/src/main/java/javax/microedition/shell/MidletSystem.java b/app/src/main/java/javax/microedition/shell/MidletSystem.java index ad4bca441..b594bb15d 100644 --- a/app/src/main/java/javax/microedition/shell/MidletSystem.java +++ b/app/src/main/java/javax/microedition/shell/MidletSystem.java @@ -35,4 +35,19 @@ public static String getProperty(String key, String def) { return value; } + /** + * Handles an advisory garbage-collection request made by a MIDlet. + * + *

Forwarding every guest request to Android can start a compacting collection inside hot + * MIDlet loops. ART still collects normally when allocation pressure requires it.

+ */ + public static void gc() { + // Java ME only requires the VM to make a best effort, so the emulator may ignore the hint. + } + + /** Preserves the receiver null check while suppressing {@link Runtime#gc()} from guest code. */ + public static void gc(Runtime runtime) { + if (runtime == null) throw new NullPointerException("runtime"); + } + } diff --git a/app/src/main/java/javax/microedition/shell/timing/TimingTransformMetadata.java b/app/src/main/java/javax/microedition/shell/timing/TimingTransformMetadata.java deleted file mode 100644 index 989c3f93d..000000000 --- a/app/src/main/java/javax/microedition/shell/timing/TimingTransformMetadata.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package javax.microedition.shell.timing; - -import java.util.Map; - -import javax.microedition.shell.GuestTimingBridge; - -/** - * Timing-specific compatibility marker stored in the converted descriptor next to the DEX - * artifact. - * - *

This is intentionally not a universal "converted" bit. A future bytecode feature must use - * its own namespaced transform/ABI entries, or participate in an explicit transform manifest, so - * that enabling or revving one feature cannot make an unrelated transformed artifact appear - * compatible.

- */ -public final class TimingTransformMetadata { - /** Version 6 virtualizes Date.class.newInstance() with caller-aware access checks. */ - public static final int TRANSFORM_VERSION = 6; - public static final String TRANSFORM_VERSION_ATTRIBUTE = "JLMod-Timing-Transform-Version"; - public static final String BRIDGE_ABI_ATTRIBUTE = "JLMod-Timing-Bridge-ABI"; - - private TimingTransformMetadata() { - } - - public static void mark(Map attributes) { - if (attributes == null) { - throw new NullPointerException("attributes"); - } - attributes.put(TRANSFORM_VERSION_ATTRIBUTE, Integer.toString(TRANSFORM_VERSION)); - attributes.put(BRIDGE_ABI_ATTRIBUTE, Integer.toString(GuestTimingBridge.ABI_VERSION)); - } - - public static boolean isCompatible(Map attributes) { - if (attributes == null) { - return false; - } - return Integer.toString(TRANSFORM_VERSION).equals( - attributes.get(TRANSFORM_VERSION_ATTRIBUTE)) - && Integer.toString(GuestTimingBridge.ABI_VERSION).equals( - attributes.get(BRIDGE_ABI_ATTRIBUTE)); - } -} diff --git a/app/src/main/java/javax/microedition/shell/transform/MidletTransformMetadata.java b/app/src/main/java/javax/microedition/shell/transform/MidletTransformMetadata.java new file mode 100644 index 000000000..24d77135e --- /dev/null +++ b/app/src/main/java/javax/microedition/shell/transform/MidletTransformMetadata.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.microedition.shell.transform; + +import java.util.Map; + +/** + * Universal compatibility marker for an installed MIDlet's transformed bytecode. + * + *

Increment {@link #TRANSFORM_VERSION} whenever the converter changes emitted bytecode or a + * runtime bridge contract used by that bytecode. Installed MIDlets with an older or missing marker + * are then rebuilt by the normal app reconversion path when their retained source is available.

+ */ +public final class MidletTransformMetadata { + /** Version 1 includes guest-time virtualization and suppression of advisory explicit GC. */ + public static final int TRANSFORM_VERSION = 1; + public static final String TRANSFORM_VERSION_ATTRIBUTE = "JLMod-Transform-Version"; + private static final String LEGACY_TIMING_TRANSFORM_ATTRIBUTE = + "JLMod-Timing-Transform-Version"; + private static final String LEGACY_TIMING_BRIDGE_ATTRIBUTE = "JLMod-Timing-Bridge-ABI"; + + private MidletTransformMetadata() { + } + + public static void mark(Map attributes) { + if (attributes == null) { + throw new NullPointerException("attributes"); + } + attributes.remove(LEGACY_TIMING_TRANSFORM_ATTRIBUTE); + attributes.remove(LEGACY_TIMING_BRIDGE_ATTRIBUTE); + attributes.put(TRANSFORM_VERSION_ATTRIBUTE, Integer.toString(TRANSFORM_VERSION)); + } + + public static boolean isCompatible(Map attributes) { + return attributes != null + && Integer.toString(TRANSFORM_VERSION).equals( + attributes.get(TRANSFORM_VERSION_ATTRIBUTE)); + } +} diff --git a/app/src/main/java/ru/playsoftware/j2meloader/config/ConfigActivity.java b/app/src/main/java/ru/playsoftware/j2meloader/config/ConfigActivity.java index fdaaef101..bd8233a0f 100644 --- a/app/src/main/java/ru/playsoftware/j2meloader/config/ConfigActivity.java +++ b/app/src/main/java/ru/playsoftware/j2meloader/config/ConfigActivity.java @@ -53,7 +53,7 @@ import java.util.List; import java.util.Set; -import javax.microedition.shell.timing.TimingTransformMetadata; +import javax.microedition.shell.transform.MidletTransformMetadata; import javax.microedition.util.ContextHolder; import kotlin.io.FilesKt; @@ -996,7 +996,7 @@ private boolean hasCompatibleTimingTransform() { try { Descriptor descriptor = new Descriptor( new File(appDir, Config.MIDLET_MANIFEST_FILE), false); - return TimingTransformMetadata.isCompatible(descriptor.getAttrs()); + return MidletTransformMetadata.isCompatible(descriptor.getAttrs()); } catch (IOException | RuntimeException e) { return false; } diff --git a/app/src/main/java/ru/woesss/j2me/installer/AppInstaller.java b/app/src/main/java/ru/woesss/j2me/installer/AppInstaller.java index 78b7e7913..e1b1b24f9 100644 --- a/app/src/main/java/ru/woesss/j2me/installer/AppInstaller.java +++ b/app/src/main/java/ru/woesss/j2me/installer/AppInstaller.java @@ -62,7 +62,7 @@ import ru.woesss.j2me.jar.Descriptor; import ru.woesss.util.TextUtils; import ru.woesss.util.zip.ZipFile; -import javax.microedition.shell.timing.TimingTransformMetadata; +import javax.microedition.shell.transform.MidletTransformMetadata; public class AppInstaller { private static final String TAG = AppInstaller.class.getSimpleName(); @@ -431,7 +431,7 @@ void install(SingleEmitter emitter) throws ConverterException, IOExcept manifest.merge(newDesc); newDesc = manifest; } - TimingTransformMetadata.mark(newDesc.getAttrs()); + MidletTransformMetadata.mark(newDesc.getAttrs()); File resJar = child(tmpDir, Config.MIDLET_RES_FILE); FileUtils.copyFileUsingChannel(srcJar, resJar); diff --git a/app/src/main/java/ru/woesss/j2me/installer/AppReconverter.java b/app/src/main/java/ru/woesss/j2me/installer/AppReconverter.java index 0c5a9782c..40211f3b9 100644 --- a/app/src/main/java/ru/woesss/j2me/installer/AppReconverter.java +++ b/app/src/main/java/ru/woesss/j2me/installer/AppReconverter.java @@ -27,7 +27,7 @@ import java.io.IOException; import java.util.jar.JarFile; -import javax.microedition.shell.timing.TimingTransformMetadata; +import javax.microedition.shell.transform.MidletTransformMetadata; import ru.playsoftware.j2meloader.config.Config; import ru.playsoftware.j2meloader.librarydb.LibraryIconOverride; @@ -61,9 +61,9 @@ public static boolean needsReconversion(File appDir) { if (payload == null || !Config.isUsableFile(descriptorFile)) return true; try { Descriptor descriptor = new Descriptor(descriptorFile, false); - return !TimingTransformMetadata.isCompatible(descriptor.getAttrs()); + return !MidletTransformMetadata.isCompatible(descriptor.getAttrs()); } catch (IOException | RuntimeException error) { - Log.w(TAG, "Unable to validate converted timing marker: " + appDir, error); + Log.w(TAG, "Unable to validate converted transform marker: " + appDir, error); return true; } } @@ -129,7 +129,7 @@ public static void reconvert(File requestedAppDir) throws IOException, Converter if (!generatedPayload.isFile() || generatedPayload.length() <= 0L) { throw new ConverterException("DX produced no converted MIDlet payload"); } - TimingTransformMetadata.mark(descriptor.getAttrs()); + MidletTransformMetadata.mark(descriptor.getAttrs()); FileUtils.copyFileUsingChannel(retainedJar, fileWithSuffix(staging, Config.MIDLET_RES_FILE)); extractIcon(descriptor, retainedJar, staging); descriptor.writeTo(fileWithSuffix(staging, Config.MIDLET_MANIFEST_FILE)); diff --git a/app/src/test/java/javax/microedition/shell/MidletSystemTest.java b/app/src/test/java/javax/microedition/shell/MidletSystemTest.java new file mode 100644 index 000000000..7e6f94f9c --- /dev/null +++ b/app/src/test/java/javax/microedition/shell/MidletSystemTest.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.microedition.shell; + +import org.junit.Test; + +public class MidletSystemTest { + @Test + public void advisoryGcRequestsReturnWithoutCollecting() { + MidletSystem.gc(); + MidletSystem.gc(Runtime.getRuntime()); + } + + @Test(expected = NullPointerException.class) + public void runtimeGcBridgePreservesReceiverNullCheck() { + MidletSystem.gc((Runtime) null); + } +} diff --git a/app/src/test/java/javax/microedition/shell/timing/TimingTransformMetadataTest.java b/app/src/test/java/javax/microedition/shell/timing/TimingTransformMetadataTest.java deleted file mode 100644 index 3f620761f..000000000 --- a/app/src/test/java/javax/microedition/shell/timing/TimingTransformMetadataTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package javax.microedition.shell.timing; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; - -public class TimingTransformMetadataTest { - @Test - public void markedMetadataIsCompatible() { - Map attributes = new HashMap<>(); - TimingTransformMetadata.mark(attributes); - - assertTrue(TimingTransformMetadata.isCompatible(attributes)); - } - - @Test - public void missingOrChangedMetadataIsRejected() { - Map attributes = new HashMap<>(); - TimingTransformMetadata.mark(attributes); - attributes.put(TimingTransformMetadata.BRIDGE_ABI_ATTRIBUTE, "999"); - - assertFalse(TimingTransformMetadata.isCompatible(attributes)); - assertFalse(TimingTransformMetadata.isCompatible(null)); - } - - @Test - public void previousTransformerVersionIsRejectedForAutomaticReconversion() { - Map attributes = new HashMap<>(); - attributes.put(TimingTransformMetadata.TRANSFORM_VERSION_ATTRIBUTE, "4"); - attributes.put(TimingTransformMetadata.BRIDGE_ABI_ATTRIBUTE, "4"); - - assertFalse(TimingTransformMetadata.isCompatible(attributes)); - } - - @Test - public void versionFiveIsRejectedAfterCallerAwareReflectionRewrite() { - Map attributes = new HashMap<>(); - attributes.put(TimingTransformMetadata.TRANSFORM_VERSION_ATTRIBUTE, "5"); - attributes.put(TimingTransformMetadata.BRIDGE_ABI_ATTRIBUTE, "4"); - - assertFalse(TimingTransformMetadata.isCompatible(attributes)); - } -} diff --git a/app/src/test/java/javax/microedition/shell/transform/MidletTransformMetadataTest.java b/app/src/test/java/javax/microedition/shell/transform/MidletTransformMetadataTest.java new file mode 100644 index 000000000..62a2ac199 --- /dev/null +++ b/app/src/test/java/javax/microedition/shell/transform/MidletTransformMetadataTest.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.microedition.shell.transform; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class MidletTransformMetadataTest { + @Test + public void currentVersionIsCompatibleAfterMarking() { + Map attributes = new HashMap<>(); + MidletTransformMetadata.mark(attributes); + + assertTrue(MidletTransformMetadata.isCompatible(attributes)); + } + + @Test + public void missingOrChangedUniversalVersionIsRejected() { + Map attributes = new HashMap<>(); + MidletTransformMetadata.mark(attributes); + attributes.put(MidletTransformMetadata.TRANSFORM_VERSION_ATTRIBUTE, "999"); + + assertFalse(MidletTransformMetadata.isCompatible(attributes)); + assertFalse(MidletTransformMetadata.isCompatible(new HashMap<>())); + assertFalse(MidletTransformMetadata.isCompatible(null)); + } + + @Test + public void legacyTimingOnlyMarkerTriggersUniversalReconversion() { + Map attributes = new HashMap<>(); + attributes.put("JLMod-Timing-Transform-Version", "6"); + attributes.put("JLMod-Timing-Bridge-ABI", "4"); + + assertFalse(MidletTransformMetadata.isCompatible(attributes)); + + MidletTransformMetadata.mark(attributes); + + assertTrue(MidletTransformMetadata.isCompatible(attributes)); + assertNull(attributes.get("JLMod-Timing-Transform-Version")); + assertNull(attributes.get("JLMod-Timing-Bridge-ABI")); + } +} diff --git a/dexlib/src/main/java/org/microemu/android/asm/AndroidMethodVisitor.java b/dexlib/src/main/java/org/microemu/android/asm/AndroidMethodVisitor.java index 9019795b6..29c5f2edd 100644 --- a/dexlib/src/main/java/org/microemu/android/asm/AndroidMethodVisitor.java +++ b/dexlib/src/main/java/org/microemu/android/asm/AndroidMethodVisitor.java @@ -24,7 +24,7 @@ * limitations. * * Modified in JL-Mod Plus to preserve Java ME bytecode return semantics and route guest timing - * call sites through the parent-owned emulator bridge during DEX conversion. + * and runtime call sites through emulator-owned bridges during DEX conversion. * * @version $Id$ */ @@ -240,7 +240,20 @@ public void visitMethodInsn(int opcode, String owner, String name, String desc, name = "setListenerCompat"; } break; + case "java/lang/Runtime": + if (opcode == INVOKEVIRTUAL && name.equals("gc") && desc.equals("()V")) { + // The static overload consumes the original receiver and preserves its null check. + mv.visitMethodInsn(INVOKESTATIC, "javax/microedition/shell/MidletSystem", + "gc", "(Ljava/lang/Runtime;)V", false); + return; + } + break; case "java/lang/System": + if (opcode == INVOKESTATIC && name.equals("gc") && desc.equals("()V")) { + mv.visitMethodInsn(INVOKESTATIC, "javax/microedition/shell/MidletSystem", + "gc", "()V", false); + return; + } if (opcode == INVOKESTATIC && name.equals("currentTimeMillis") && desc.equals("()J")) { mv.visitMethodInsn(INVOKESTATIC, "javax/microedition/shell/GuestTimingBridge", "currentTimeMillis", "()J", false); diff --git a/dexlib/src/test/java/org/microemu/android/asm/AndroidMethodVisitorTest.java b/dexlib/src/test/java/org/microemu/android/asm/AndroidMethodVisitorTest.java index 189637390..30aa54940 100644 --- a/dexlib/src/test/java/org/microemu/android/asm/AndroidMethodVisitorTest.java +++ b/dexlib/src/test/java/org/microemu/android/asm/AndroidMethodVisitorTest.java @@ -42,6 +42,22 @@ public void guestClockAndExplicitSleepUseParentOwnedBridge() { assertEquals(4, calls.size()); } + @Test + public void guestExplicitGcUsesAdvisoryEmulatorBridge() { + byte[] transformed = transform(createExplicitGcClass()); + List calls = methodCalls(transformed, "collect"); + + assertTrue(calls.contains( + "INVOKESTATIC javax/microedition/shell/MidletSystem.gc()V")); + assertTrue(calls.contains( + "INVOKESTATIC java/lang/Runtime.getRuntime()Ljava/lang/Runtime;")); + assertTrue(calls.contains( + "INVOKESTATIC javax/microedition/shell/MidletSystem.gc(Ljava/lang/Runtime;)V")); + assertFalse(calls.contains("INVOKESTATIC java/lang/System.gc()V")); + assertFalse(calls.contains("INVOKEVIRTUAL java/lang/Runtime.gc()V")); + assertEquals(3, calls.size()); + } + @Test public void yieldRewriteUsesNonThrowingBridge() { byte[] source = createYieldClass(); @@ -164,6 +180,24 @@ private static byte[] createYieldClass() { return writer.toByteArray(); } + private static byte[] createExplicitGcClass() { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); + writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, "sample/ExplicitGc", null, + "java/lang/Object", null); + MethodVisitor method = writer.visitMethod( + Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "collect", "()V", null, null); + method.visitCode(); + method.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "gc", "()V", false); + method.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime", "getRuntime", + "()Ljava/lang/Runtime;", false); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime", "gc", "()V", false); + method.visitInsn(Opcodes.RETURN); + method.visitMaxs(1, 0); + method.visitEnd(); + writer.visitEnd(); + return writer.toByteArray(); + } + private static byte[] createWaitClass() { ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, "sample/Wait", null, "java/lang/Object", null); diff --git a/dexlib/src/test/java/org/microemu/android/asm/AndroidProducerDexTest.java b/dexlib/src/test/java/org/microemu/android/asm/AndroidProducerDexTest.java index e76fdc453..c00c6fa7e 100644 --- a/dexlib/src/test/java/org/microemu/android/asm/AndroidProducerDexTest.java +++ b/dexlib/src/test/java/org/microemu/android/asm/AndroidProducerDexTest.java @@ -34,7 +34,7 @@ public class AndroidProducerDexTest { @Test - public void transformedTimingCallsitesProduceReadableDex() throws Exception { + public void transformedGuestCallsitesProduceReadableDex() throws Exception { Path root = Files.createTempDirectory("jlmod-dex-transform-"); Path classDirectory = Files.createDirectories(root.resolve("sample")); Path classFile = classDirectory.resolve("Timing.class"); @@ -63,6 +63,10 @@ public void transformedTimingCallsitesProduceReadableDex() throws Exception { // allocation/constructor verifier shape valid for subclasses and non-canonical code. assertEquals(1, countMethods(dex, bridge, "currentTimeMillis")); assertEquals(2, countMethods(dex, bridge, "calendarInstance")); + String systemBridge = "Ljavax/microedition/shell/MidletSystem;"; + assertEquals(2, countMethods(dex, systemBridge, "gc")); + assertEquals(0, countMethods(dex, "Ljava/lang/System;", "gc")); + assertEquals(0, countMethods(dex, "Ljava/lang/Runtime;", "gc")); } finally { Files.deleteIfExists(dexFile); Files.deleteIfExists(classFile); @@ -116,12 +120,26 @@ private static byte[] createTimingClass() { writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "sample/Timing", null, "java/lang/Object", null); createClockAndSleepMethod(writer); + createExplicitGcMethod(writer); createWaitMethod(writer); createDateAndCalendarMethod(writer); writer.visitEnd(); return writer.toByteArray(); } + private static void createExplicitGcMethod(ClassWriter writer) { + MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, + "collect", "()V", null, null); + method.visitCode(); + method.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "gc", "()V", false); + method.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime", "getRuntime", + "()Ljava/lang/Runtime;", false); + method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime", "gc", "()V", false); + method.visitInsn(Opcodes.RETURN); + method.visitMaxs(1, 0); + method.visitEnd(); + } + private static byte[] createSimpleClass(String name) { ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null,