diff --git a/compiler/mx.compiler/suite.py b/compiler/mx.compiler/suite.py index 8381780ebf95..bc19271c4545 100644 --- a/compiler/mx.compiler/suite.py +++ b/compiler/mx.compiler/suite.py @@ -557,8 +557,8 @@ org.graalvm.nativeimage.foreign, org.graalvm.nativeimage.llvm, com.oracle.svm.svm_enterprise, + com.oracle.svm.jdwp.resident, com.oracle.svm_enterprise.ml_dataset, - com.oracle.svm.enterprise.jdwp.resident, org.graalvm.nativeimage.base, org.graalvm.extraimage.builder, org.graalvm.extraimage.librarysupport, diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index eb5afcd249d4..e8f4dd2a3de8 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -1626,6 +1626,42 @@ def prevent_build_path_in_libgraal(): ) mx_sdk_vm.register_graalvm_component(libgraal) +libsvmjdwp_build_args = [ + "-H:+UnlockExperimentalVMOptions", + "-H:+IncludeDebugHelperMethods", + "-H:-DeleteLocalSymbols", + "-H:+PreserveFramePointer", +] + +libsvmjdwp_lib_config = mx_sdk_vm.LibraryConfig( + destination="", + jvm_library=True, + use_modules='image', + jar_distributions=['substratevm:SVM_JDWP_SERVER'], + build_args=libsvmjdwp_build_args + [ + '--features=com.oracle.svm.jdwp.server.ServerJDWPFeature', + ], + headers=False, +) + +libsvmjdwp = mx_sdk_vm.GraalVmJreComponent( + suite=suite, + name='SubstrateVM JDWP Debugger', + short_name='svmjdwp', + dir_name="svm", + license_files=[], + third_party_license_files=[], + dependencies=[], + jar_distributions=[], + builder_jar_distributions=['substratevm:SVM_JDWP_COMMON', 'substratevm:SVM_JDWP_RESIDENT'], + support_distributions=[], + priority=1, + library_configs=[libsvmjdwp_lib_config], + stability="experimental", + jlink=False, +) +mx_sdk_vm.register_graalvm_component(libsvmjdwp) + def _native_image_configure_extra_jvm_args(): packages = ['jdk.graal.compiler/jdk.graal.compiler.phases.common', 'jdk.internal.vm.ci/jdk.vm.ci.meta', 'jdk.internal.vm.ci/jdk.vm.ci.services', 'jdk.graal.compiler/jdk.graal.compiler.core.common.util'] args = ['--add-exports=' + packageName + '=ALL-UNNAMED' for packageName in packages] diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index e0f64fb535a2..ae25f5a00015 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -1537,6 +1537,119 @@ "jacoco" : "exclude", "graalCompilerSourceEdition": "ignore", }, + + "com.oracle.svm.interpreter.metadata": { + "subDir": "src", + "sourceDirs": ["src"], + "dependencies": [ + "substratevm:SVM" + ], + "requiresConcealed" : { + "jdk.internal.vm.ci" : [ + "jdk.vm.ci.meta", + ], + }, + "checkstyle": "com.oracle.svm.hosted", + "javaCompliance": "21+", + "workingSets": "SVM", + }, + + "com.oracle.svm.interpreter": { + "subDir": "src", + "sourceDirs": ["src"], + "dependencies": [ + "com.oracle.svm.interpreter.metadata", + ], + "requires" : [ + "java.base" + ], + "requiresConcealed" : { + "jdk.internal.vm.ci" : [ + "jdk.vm.ci.meta", + "jdk.vm.ci.code", + ], + "java.base" : [ + "jdk.internal.misc", # Unsafe + ], + }, + "checkstyle": "com.oracle.svm.hosted", + "javaCompliance": "21+", + "annotationProcessors": [ + "compiler:GRAAL_PROCESSOR", + "substratevm:SVM_PROCESSOR", + ], + "workingSets": "SVM", + }, + + # Common project both jdwp.server and jdwp.resident. + "com.oracle.svm.jdwp.bridge": { + "subDir": "src", + "sourceDirs": ["src"], + "dependencies": [ + "substratevm:SVM", + ], + "requiresConcealed" : { + "jdk.internal.vm.ci" : [ + "jdk.vm.ci.meta", + ], + }, + "checkstyle": "com.oracle.svm.hosted", + "javaCompliance": "21+", + "annotationProcessors": [ + "compiler:GRAAL_PROCESSOR", + "substratevm:SVM_PROCESSOR", + ], + "workingSets": "SVM", + }, + + # JDWP server, should run on HotSpot and as a shared library e.g. libsvmjdwp.so + "com.oracle.svm.jdwp.server": { + "subDir": "src", + "sourceDirs": ["src"], + "dependencies": [ + "com.oracle.svm.interpreter.metadata", + "com.oracle.svm.jdwp.bridge", + ], + "requiresConcealed" : { + "jdk.internal.vm.ci" : [ + "jdk.vm.ci.meta", + ], + "java.base" : [ + "jdk.internal.misc", # Signal + ], + }, + "checkstyle": "com.oracle.svm.hosted", + "javaCompliance": "21+", + "annotationProcessors": [ + "substratevm:SVM_PROCESSOR", + ], + "workingSets": "SVM", + }, + + # JDWP implementation bits that are included in the application. + "com.oracle.svm.jdwp.resident": { + "subDir": "src", + "sourceDirs": ["src"], + "dependencies": [ + "com.oracle.svm.interpreter", + "com.oracle.svm.jdwp.bridge", + ], + "requiresConcealed" : { + "jdk.internal.vm.ci" : [ + "jdk.vm.ci.meta", + "jdk.vm.ci.code", + ], + "java.base" : [ + "jdk.internal.misc", # Signal + ], + }, + "checkstyle": "com.oracle.svm.hosted", + "javaCompliance": "21+", + "annotationProcessors": [ + "substratevm:SVM_PROCESSOR", + ], + "workingSets": "SVM", + }, }, "distributions": { @@ -1596,9 +1709,9 @@ org.graalvm.extraimage.librarysupport, com.oracle.svm.extraimage_enterprise, org.graalvm.nativeimage.foreign, - com.oracle.svm.enterprise.jdwp.common, - com.oracle.svm.enterprise.jdwp.server, - com.oracle.svm.enterprise.jdwp.resident, + com.oracle.svm.jdwp.common, + com.oracle.svm.jdwp.server, + com.oracle.svm.jdwp.resident, org.graalvm.truffle.runtime.svm, com.oracle.truffle.enterprise.svm""", "com.oracle.svm.hosted.c.libc to com.oracle.graal.sandbox", @@ -1611,7 +1724,7 @@ "com.oracle.svm.hosted.fieldfolding to jdk.graal.compiler", "com.oracle.svm.hosted.phases to jdk.graal.compiler", "com.oracle.svm.hosted.reflect to jdk.graal.compiler", - "com.oracle.svm.core.thread to com.oracle.svm.enterprise.jdwp.resident", + "com.oracle.svm.core.thread to com.oracle.svm.jdwp.resident", ], "requires": [ "java.management", @@ -2079,7 +2192,7 @@ "org.graalvm.collections", ], "exports" : [ - "com.oracle.svm.util to org.graalvm.nativeimage.pointsto,org.graalvm.nativeimage.builder,org.graalvm.nativeimage.librarysupport,org.graalvm.nativeimage.driver,org.graalvm.nativeimage.llvm,org.graalvm.nativeimage.agent.jvmtibase,org.graalvm.nativeimage.agent.tracing,org.graalvm.nativeimage.agent.diagnostics,org.graalvm.nativeimage.junitsupport,com.oracle.svm.svm_enterprise,com.oracle.svm_enterprise.ml_dataset,com.oracle.svm.enterprise.jdwp.resident,org.graalvm.extraimage.builder,com.oracle.svm.extraimage_enterprise,org.graalvm.extraimage.librarysupport,org.graalvm.nativeimage.foreign,org.graalvm.truffle.runtime.svm,com.oracle.truffle.enterprise.svm", + "com.oracle.svm.util to org.graalvm.nativeimage.pointsto,org.graalvm.nativeimage.builder,org.graalvm.nativeimage.librarysupport,org.graalvm.nativeimage.driver,org.graalvm.nativeimage.llvm,org.graalvm.nativeimage.agent.jvmtibase,org.graalvm.nativeimage.agent.tracing,org.graalvm.nativeimage.agent.diagnostics,org.graalvm.nativeimage.junitsupport,com.oracle.svm.svm_enterprise,com.oracle.svm_enterprise.ml_dataset,com.oracle.svm.jdwp.resident,org.graalvm.extraimage.builder,com.oracle.svm.extraimage_enterprise,org.graalvm.extraimage.librarysupport,org.graalvm.nativeimage.foreign,org.graalvm.truffle.runtime.svm,com.oracle.truffle.enterprise.svm", "com.oracle.svm.common.meta to org.graalvm.nativeimage.pointsto,org.graalvm.nativeimage.builder,org.graalvm.nativeimage.llvm,org.graalvm.extraimage.builder,org.graalvm.nativeimage.foreign,org.graalvm.truffle.runtime.svm,com.oracle.truffle.enterprise.svm", "com.oracle.svm.common.option to org.graalvm.nativeimage.pointsto,org.graalvm.nativeimage.builder,org.graalvm.nativeimage.driver,org.graalvm.nativeimage.foreign,org.graalvm.truffle.runtime.svm,com.oracle.truffle.enterprise.svm", ], @@ -2407,5 +2520,62 @@ "tag": ["default", "public"], }, }, + + "SVM_JDWP_COMMON": { + "subDir": "src", + "dependencies": [ + "com.oracle.svm.interpreter.metadata", + "com.oracle.svm.jdwp.bridge", + ], + "distDependencies": [ + "SVM", + ], + "moduleInfo" : { + "name" : "com.oracle.svm.jdwp.common", + "exports" : [ + "com.oracle.svm.jdwp.bridge to com.oracle.svm.jdwp.server,com.oracle.svm.jdwp.resident", + "com.oracle.svm.jdwp.bridge.nativebridge to com.oracle.svm.jdwp.server,com.oracle.svm.jdwp.resident", + "com.oracle.svm.jdwp.bridge.jniutils to com.oracle.svm.jdwp.server,com.oracle.svm.jdwp.resident", + "com.oracle.svm.interpreter.metadata to com.oracle.svm.jdwp.server,com.oracle.svm.jdwp.resident", + "com.oracle.svm.interpreter.metadata.serialization to com.oracle.svm.jdwp.server,com.oracle.svm.jdwp.resident", + ], + "requires" : [ + "org.graalvm.collections", + ], + } + }, + + "SVM_JDWP_RESIDENT": { + "subDir": "src", + "dependencies": [ + "com.oracle.svm.jdwp.resident", + ], + "distDependencies": [ + "SVM_JDWP_COMMON", + "sdk:COLLECTIONS", + "compiler:GRAAL", + ], + "moduleInfo" : { + "name" : "com.oracle.svm.jdwp.resident", + "exports": [ + "com.oracle.svm.interpreter,com.oracle.svm.jdwp.resident to org.graalvm.nativeimage.builder", + ], + } + }, + + "SVM_JDWP_SERVER": { + "subDir": "src", + "dependencies": [ + "com.oracle.svm.jdwp.server", + ], + "distDependencies": [ + "substratevm:SVM", + "SVM_JDWP_COMMON", + ], + "moduleInfo" : { + "name" : "com.oracle.svm.jdwp.server", + } + }, + }, } diff --git a/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/AArch64InterpreterStubs.java b/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/AArch64InterpreterStubs.java new file mode 100644 index 000000000000..4dfae74a7c6e --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/AArch64InterpreterStubs.java @@ -0,0 +1,632 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.graal.aarch64; + +import com.oracle.svm.core.c.struct.OffsetOf; +import com.oracle.svm.core.deopt.DeoptimizationSlotPacking; +import com.oracle.svm.core.graal.code.InterpreterAccessStubData; +import com.oracle.svm.core.meta.SharedMethod; +import com.oracle.svm.core.util.VMError; +import jdk.graal.compiler.api.replacements.Fold; +import jdk.graal.compiler.asm.Label; +import jdk.graal.compiler.asm.aarch64.AArch64Address; +import jdk.graal.compiler.asm.aarch64.AArch64MacroAssembler; +import jdk.graal.compiler.core.common.NumUtil; +import jdk.graal.compiler.lir.asm.CompilationResultBuilder; +import jdk.vm.ci.aarch64.AArch64; +import jdk.vm.ci.code.Register; +import jdk.vm.ci.code.RegisterValue; +import jdk.vm.ci.code.StackSlot; +import jdk.vm.ci.meta.AllocatableValue; +import org.graalvm.nativeimage.c.struct.RawField; +import org.graalvm.nativeimage.c.struct.RawStructure; +import org.graalvm.nativeimage.c.struct.SizeOf; +import org.graalvm.word.Pointer; +import org.graalvm.word.PointerBase; +import org.graalvm.word.WordFactory; + +import static jdk.graal.compiler.asm.aarch64.AArch64Address.AddressingMode.IMMEDIATE_POST_INDEXED; +import static jdk.graal.compiler.asm.aarch64.AArch64Address.AddressingMode.IMMEDIATE_SIGNED_UNSCALED; +import static jdk.graal.compiler.asm.aarch64.AArch64Address.AddressingMode.IMMEDIATE_UNSIGNED_SCALED; +import static jdk.graal.compiler.asm.aarch64.AArch64Address.createImmediateAddress; +import static jdk.vm.ci.aarch64.AArch64.r0; +import static jdk.vm.ci.aarch64.AArch64.r1; +import static jdk.vm.ci.aarch64.AArch64.r2; +import static jdk.vm.ci.aarch64.AArch64.r3; +import static jdk.vm.ci.aarch64.AArch64.r4; +import static jdk.vm.ci.aarch64.AArch64.r5; +import static jdk.vm.ci.aarch64.AArch64.r6; +import static jdk.vm.ci.aarch64.AArch64.r7; +import static jdk.vm.ci.aarch64.AArch64.sp; +import static jdk.vm.ci.aarch64.AArch64.v0; +import static jdk.vm.ci.aarch64.AArch64.v1; +import static jdk.vm.ci.aarch64.AArch64.v2; +import static jdk.vm.ci.aarch64.AArch64.v3; +import static jdk.vm.ci.aarch64.AArch64.v4; +import static jdk.vm.ci.aarch64.AArch64.v5; +import static jdk.vm.ci.aarch64.AArch64.v6; +import static jdk.vm.ci.aarch64.AArch64.v7; + +public class AArch64InterpreterStubs { + + public static final Register TRAMPOLINE_ARGUMENT = AArch64.r10; + + public static class InterpreterEnterStubContext extends SubstrateAArch64Backend.SubstrateAArch64FrameContext { + + public InterpreterEnterStubContext(SharedMethod method) { + super(method); + } + + private static AArch64Address createImmediate(int offset) { + int deoptSlotSize = 8 + 8 /* padding */; + return createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, sp, deoptSlotSize + offset); + } + + @Override + public void enter(CompilationResultBuilder crb) { + AArch64MacroAssembler masm = (AArch64MacroAssembler) crb.asm; + + Register trampArg = TRAMPOLINE_ARGUMENT; + Register spCopy = AArch64.r11; + + masm.mov(64, spCopy, sp); + + super.enter(crb); + + /* sp points to InterpreterData struct */ + masm.str(64, spCopy, createImmediate(offsetAbiSpReg())); + + masm.str(64, r0, createImmediate(offsetAbiGpArg0())); + masm.str(64, r1, createImmediate(offsetAbiGpArg1())); + masm.str(64, r2, createImmediate(offsetAbiGpArg2())); + masm.str(64, r3, createImmediate(offsetAbiGpArg3())); + masm.str(64, r4, createImmediate(offsetAbiGpArg4())); + masm.str(64, r5, createImmediate(offsetAbiGpArg5())); + masm.str(64, r6, createImmediate(offsetAbiGpArg6())); + masm.str(64, r7, createImmediate(offsetAbiGpArg7())); + + masm.fstr(64, v0, createImmediate(offsetAbiFpArg0())); + masm.fstr(64, v1, createImmediate(offsetAbiFpArg1())); + masm.fstr(64, v2, createImmediate(offsetAbiFpArg2())); + masm.fstr(64, v3, createImmediate(offsetAbiFpArg3())); + masm.fstr(64, v4, createImmediate(offsetAbiFpArg4())); + masm.fstr(64, v5, createImmediate(offsetAbiFpArg5())); + masm.fstr(64, v6, createImmediate(offsetAbiFpArg6())); + masm.fstr(64, v7, createImmediate(offsetAbiFpArg7())); + + /* sp+deoptSlotSize points to InterpreterData struct */ + masm.add(64, r1, sp, 16 /* deoptSlotSize */); + + /* Pass the interpreter method index as first arg */ + masm.mov(64, r0, trampArg); + } + + @Override + public void leave(CompilationResultBuilder crb) { + AArch64MacroAssembler masm = (AArch64MacroAssembler) crb.asm; + + /* r0 is a pointer to InterpreterEnterData */ + + /* Move fp return value into ABI register */ + masm.fldr(64, v0, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, r0, offsetAbiFpRet())); + + /* Move gp return value into ABI register */ + masm.ldr(64, r0, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, r0, offsetAbiGpRet())); + + super.leave(crb); + } + } + + public static class InterpreterLeaveStubContext extends SubstrateAArch64Backend.SubstrateAArch64FrameContext { + + public InterpreterLeaveStubContext(SharedMethod method) { + super(method); + } + + @Override + public void enter(CompilationResultBuilder crb) { + super.enter(crb); + AArch64MacroAssembler masm = (AArch64MacroAssembler) crb.asm; + + /* sp points to four reserved stack slots for this stub */ + + /* Pointer to InterpreterData struct */ + masm.str(64, r1, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, sp, 0)); + /* Variable stack size */ + masm.str(64, r2, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, sp, 8)); + /* gcReferenceMap next */ + masm.str(64, r3, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, sp, 16)); + + /* 4th slot is for stack alignment to 0x10 */ + + masm.sub(64, sp, sp, r2 /* variable stack size */); + } + + @Override + public void leave(CompilationResultBuilder crb) { + AArch64MacroAssembler masm = (AArch64MacroAssembler) crb.asm; + Register callTarget = AArch64.r10; + + /* Save call target */ + masm.mov(64, callTarget, r0); + + /* Get pointer to InterpreterData struct */ + masm.mov(64, r0, r1); + + Register stackSize = AArch64.r2; + Label regsHandling = new Label(); + /* if stackSize == 0 */ + masm.cbz(64, stackSize, regsHandling); + + /* Copy prepared outgoing args to the stack where the ABI expects it */ + Register calleeSpArgs = AArch64.r4; + Register interpDataSp = AArch64.r1; + masm.ldr(64, interpDataSp, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, r0, offsetAbiSpReg())); + masm.mov(64, calleeSpArgs, sp); + + int wordSize = 8; + Label spCopyBegin = new Label(); + masm.bind(spCopyBegin); + masm.ldr(64, r3, createImmediateAddress(64, IMMEDIATE_POST_INDEXED, interpDataSp, wordSize)); + masm.str(64, r3, createImmediateAddress(64, IMMEDIATE_POST_INDEXED, calleeSpArgs, wordSize)); + masm.sub(64, stackSize, stackSize, wordSize); + + masm.cbnz(64, stackSize, spCopyBegin); + + masm.bind(regsHandling); + + /* Set fp argument registers */ + masm.fldr(64, v7, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiFpArg7())); + masm.fldr(64, v6, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiFpArg6())); + masm.fldr(64, v5, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiFpArg5())); + masm.fldr(64, v4, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiFpArg4())); + masm.fldr(64, v3, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiFpArg3())); + masm.fldr(64, v2, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiFpArg2())); + masm.fldr(64, v1, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiFpArg1())); + masm.fldr(64, v0, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiFpArg0())); + + /* Set gp argument registers */ + masm.ldr(64, r7, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiGpArg7())); + masm.ldr(64, r6, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiGpArg6())); + masm.ldr(64, r5, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiGpArg5())); + masm.ldr(64, r4, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiGpArg4())); + masm.ldr(64, r3, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiGpArg3())); + masm.ldr(64, r2, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiGpArg2())); + masm.ldr(64, r1, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiGpArg1())); + masm.ldr(64, r0, createImmediateAddress(64, IMMEDIATE_SIGNED_UNSCALED, AArch64.r0, offsetAbiGpArg0())); + + /* Call into target method */ + masm.blr(callTarget); + + Register resultCopy = AArch64.r8; + masm.mov(64, resultCopy, r0); + + /* Obtain stack size from deopt slot */ + masm.ldr(64, r2, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, sp, 0)); + + /* Assumption of deopt slot encoding */ + assert crb.target.stackAlignment == 0x10; + masm.lsr(64, r2, r2, DeoptimizationSlotPacking.POS_VARIABLE_FRAMESIZE - DeoptimizationSlotPacking.STACK_ALIGNMENT); + + /* Restore stack pointer */ + masm.add(64, sp, sp, r2); + + /* Pointer InterpreterData struct */ + masm.ldr(64, r0, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, sp, 0)); + + /* Save gp ABI register into InterpreterData struct */ + masm.str(64, resultCopy, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, r0, offsetAbiGpRet())); + + /* Save fp ABI register into InterpreterData struct */ + masm.fstr(64, v0, createImmediateAddress(64, IMMEDIATE_UNSIGNED_SCALED, r0, offsetAbiFpRet())); + + super.leave(crb); + } + } + + public static int sizeOfInterpreterData() { + return NumUtil.roundUp(SizeOf.get(InterpreterDataAArch64.class), 0x10); + } + + public static int additionalFrameSizeEnterStub() { + int wordSize = 8; + int deoptSlotSize = wordSize + wordSize /* for padding */; + return sizeOfInterpreterData() + deoptSlotSize; + } + + public static int additionalFrameSizeLeaveStub() { + int wordSize = 8; + /* + * reserve four slots for: base address of outgoing stack args, variable stack size, + * gcReferenceMap, padding + */ + return 4 * wordSize; + } + + @RawStructure + public interface InterpreterDataAArch64 extends PointerBase { + @RawField + long getStackSize(); + + @RawField + void setStackSize(long val); + + @RawField + long getAbiSpReg(); + + @RawField + void setAbiSpReg(long val); + + /* arch specific */ + @RawField + long getAbiGpArg0(); + + @RawField + void setAbiGpArg0(long val); + + @RawField + long getAbiGpArg1(); + + @RawField + void setAbiGpArg1(long val); + + @RawField + long getAbiGpArg2(); + + @RawField + void setAbiGpArg2(long val); + + @RawField + long getAbiGpArg3(); + + @RawField + void setAbiGpArg3(long val); + + @RawField + long getAbiGpArg4(); + + @RawField + void setAbiGpArg4(long val); + + @RawField + long getAbiGpArg5(); + + @RawField + void setAbiGpArg5(long val); + + @RawField + long getAbiGpArg6(); + + @RawField + void setAbiGpArg6(long val); + + @RawField + long getAbiGpArg7(); + + @RawField + void setAbiGpArg7(long val); + + @RawField + long getAbiFpArg0(); + + @RawField + void setAbiFpArg0(long val); + + @RawField + long getAbiFpArg1(); + + @RawField + void setAbiFpArg1(long val); + + @RawField + long getAbiFpArg2(); + + @RawField + void setAbiFpArg2(long val); + + @RawField + long getAbiFpArg3(); + + @RawField + void setAbiFpArg3(long val); + + @RawField + long getAbiFpArg4(); + + @RawField + void setAbiFpArg4(long val); + + @RawField + long getAbiFpArg5(); + + @RawField + void setAbiFpArg5(long val); + + @RawField + long getAbiFpArg6(); + + @RawField + void setAbiFpArg6(long val); + + @RawField + long getAbiFpArg7(); + + @RawField + void setAbiFpArg7(long val); + + @RawField + void setAbiGpRet(long val); + + @RawField + long getAbiGpRet(); + + @RawField + void setAbiFpRet(long val); + + @RawField + long getAbiFpRet(); + } + + @Fold + public static int offsetAbiSpReg() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiSpReg"); + } + + @Fold + public static int offsetAbiGpArg0() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpArg0"); + } + + @Fold + public static int offsetAbiGpArg1() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpArg1"); + } + + @Fold + public static int offsetAbiGpArg2() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpArg2"); + } + + @Fold + public static int offsetAbiGpArg3() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpArg3"); + } + + @Fold + public static int offsetAbiGpArg4() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpArg4"); + } + + @Fold + public static int offsetAbiGpArg5() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpArg5"); + } + + @Fold + public static int offsetAbiGpArg6() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpArg6"); + } + + @Fold + public static int offsetAbiGpArg7() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpArg7"); + } + + @Fold + public static int offsetAbiFpArg0() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpArg0"); + } + + @Fold + public static int offsetAbiFpArg1() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpArg1"); + } + + @Fold + public static int offsetAbiFpArg2() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpArg2"); + } + + @Fold + public static int offsetAbiFpArg3() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpArg3"); + } + + @Fold + public static int offsetAbiFpArg4() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpArg4"); + } + + @Fold + public static int offsetAbiFpArg5() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpArg5"); + } + + @Fold + public static int offsetAbiFpArg6() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpArg6"); + } + + @Fold + public static int offsetAbiFpArg7() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpArg7"); + } + + @Fold + public static int offsetAbiFpRet() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiFpRet"); + } + + @Fold + public static int offsetAbiGpRet() { + return OffsetOf.get(InterpreterDataAArch64.class, "AbiGpRet"); + } + + public static class AArch64InterpreterAccessStubData implements InterpreterAccessStubData { + + @Override + public void setSp(Pointer data, int stackSize, Pointer stackBuffer) { + VMError.guarantee(stackBuffer.isNonNull()); + + InterpreterDataAArch64 p = (InterpreterDataAArch64) data; + p.setAbiSpReg(stackBuffer.rawValue()); + p.setStackSize(stackSize); + + /* + * We re-use the deopt slot to leave the stack size at a known place for the stack + * walker. See + * + * com.oracle.svm.core.graal.aarch64.SubstrateAArch64RegisterConfig#getCallingConvention + * + * comment above `currentStackOffset` declaration. + */ + assert stackSize % 0x10 == 0; + stackBuffer.writeLong(0, DeoptimizationSlotPacking.encodeVariableFrameSizeIntoDeoptSlot(stackSize)); + } + + @Override + public long getGpArgumentAt(AllocatableValue ccArg, Pointer data, int pos) { + InterpreterDataAArch64 p = (InterpreterDataAArch64) data; + return switch (pos) { + case 0 -> p.getAbiGpArg0(); + case 1 -> p.getAbiGpArg1(); + case 2 -> p.getAbiGpArg2(); + case 3 -> p.getAbiGpArg3(); + case 4 -> p.getAbiGpArg4(); + case 5 -> p.getAbiGpArg5(); + case 6 -> p.getAbiGpArg6(); + case 7 -> p.getAbiGpArg7(); + default -> { + StackSlot stackSlot = (StackSlot) ccArg; + Pointer spVal = WordFactory.pointer(p.getAbiSpReg()); + yield spVal.readLong(stackSlot.getOffset(0)); + } + }; + } + + @Override + public long setGpArgumentAt(AllocatableValue ccArg, Pointer data, int pos, long val) { + InterpreterDataAArch64 p = (InterpreterDataAArch64) data; + if (pos >= 0 && pos <= 7) { + VMError.guarantee(ccArg instanceof RegisterValue); + switch (pos) { + case 0 -> p.setAbiGpArg0(val); + case 1 -> p.setAbiGpArg1(val); + case 2 -> p.setAbiGpArg2(val); + case 3 -> p.setAbiGpArg3(val); + case 4 -> p.setAbiGpArg4(val); + case 5 -> p.setAbiGpArg5(val); + case 6 -> p.setAbiGpArg6(val); + case 7 -> p.setAbiGpArg7(val); + } + /* no GC mask required */ + return 0; + } + StackSlot stackSlot = (StackSlot) ccArg; + + Pointer spVal = WordFactory.pointer(p.getAbiSpReg()); + int offset = stackSlot.getOffset(0); + VMError.guarantee(spVal.isNonNull()); + VMError.guarantee(offset < p.getStackSize()); + + spVal.writeLong(offset, val); + + VMError.guarantee((pos - 8) < Long.SIZE, "more than 64 stack args are not supported"); + return 1L << (pos - 8); + } + + @Override + public long getFpArgumentAt(AllocatableValue ccArg, Pointer data, int pos) { + InterpreterDataAArch64 p = (InterpreterDataAArch64) data; + return switch (pos) { + case 0 -> p.getAbiFpArg0(); + case 1 -> p.getAbiFpArg1(); + case 2 -> p.getAbiFpArg2(); + case 3 -> p.getAbiFpArg3(); + case 4 -> p.getAbiFpArg4(); + case 5 -> p.getAbiFpArg5(); + case 6 -> p.getAbiFpArg6(); + case 7 -> p.getAbiFpArg7(); + default -> { + StackSlot stackSlot = (StackSlot) ccArg; + Pointer spVal = WordFactory.pointer(p.getAbiSpReg()); + yield spVal.readLong(stackSlot.getOffset(0)); + } + }; + } + + @Override + public void setFpArgumentAt(AllocatableValue ccArg, Pointer data, int pos, long val) { + InterpreterDataAArch64 p = (InterpreterDataAArch64) data; + switch (pos) { + case 0 -> p.setAbiFpArg0(val); + case 1 -> p.setAbiFpArg1(val); + case 2 -> p.setAbiFpArg2(val); + case 3 -> p.setAbiFpArg3(val); + case 4 -> p.setAbiFpArg4(val); + case 5 -> p.setAbiFpArg5(val); + case 6 -> p.setAbiFpArg6(val); + case 7 -> p.setAbiFpArg7(val); + default -> { + StackSlot stackSlot = (StackSlot) ccArg; + + Pointer spVal = WordFactory.pointer(p.getAbiSpReg()); + int offset = stackSlot.getOffset(0); + + VMError.guarantee(spVal.isNonNull()); + VMError.guarantee(offset < p.getStackSize()); + + spVal.writeLong(offset, val); + } + } + } + + @Override + public long getGpReturn(Pointer data) { + return ((InterpreterDataAArch64) data).getAbiGpRet(); + } + + @Override + public void setGpReturn(Pointer data, long gpReturn) { + ((InterpreterDataAArch64) data).setAbiGpRet(gpReturn); + } + + @Override + public long getFpReturn(Pointer data) { + return ((InterpreterDataAArch64) data).getAbiFpRet(); + } + + @Override + public void setFpReturn(Pointer data, long fpReturn) { + ((InterpreterDataAArch64) data).setAbiFpRet(fpReturn); + } + + @Override + @Fold + public int allocateStubDataSize() { + return sizeOfInterpreterData(); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/SubstrateAArch64Backend.java b/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/SubstrateAArch64Backend.java index b87390e83b09..37663a29b2c1 100755 --- a/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/SubstrateAArch64Backend.java +++ b/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/SubstrateAArch64Backend.java @@ -38,6 +38,7 @@ import java.util.function.BiConsumer; +import com.oracle.svm.core.interpreter.InterpreterSupport; import org.graalvm.nativeimage.ImageSingletons; import com.oracle.svm.core.FrameAccess; @@ -1244,7 +1245,14 @@ protected FrameContext createFrameContext(SharedMethod method, Deoptimizer.StubT return new DeoptEntryStubContext(method, callingConvention); } else if (stubType == Deoptimizer.StubType.ExitStub) { return new DeoptExitStubContext(method, callingConvention); + } else if (stubType == Deoptimizer.StubType.InterpreterEnterStub) { + assert InterpreterSupport.isEnabled(); + return new AArch64InterpreterStubs.InterpreterEnterStubContext(method); + } else if (stubType == Deoptimizer.StubType.InterpreterLeaveStub) { + assert InterpreterSupport.isEnabled(); + return new AArch64InterpreterStubs.InterpreterLeaveStubContext(method); } + return new SubstrateAArch64FrameContext(method); } @@ -1364,7 +1372,19 @@ protected void resetForEmittingCode(CompilationResultBuilder crb) { public LIRGenerationResult newLIRGenerationResult(CompilationIdentifier compilationId, LIR lir, RegisterAllocationConfig registerAllocationConfig, StructuredGraph graph, Object stub) { SharedMethod method = (SharedMethod) graph.method(); CallingConvention callingConvention = CodeUtil.getCallingConvention(getCodeCache(), method.getCallingConventionKind().toType(false), method, this); - return new SubstrateLIRGenerationResult(compilationId, lir, newFrameMapBuilder(registerAllocationConfig.getRegisterConfig()), registerAllocationConfig, callingConvention, method); + LIRGenerationResult lirGenerationResult = new SubstrateLIRGenerationResult(compilationId, lir, newFrameMapBuilder(registerAllocationConfig.getRegisterConfig()), registerAllocationConfig, + callingConvention, method); + + FrameMap frameMap = ((FrameMapBuilderTool) lirGenerationResult.getFrameMapBuilder()).getFrameMap(); + Deoptimizer.StubType stubType = method.getDeoptStubType(); + if (stubType == Deoptimizer.StubType.InterpreterEnterStub) { + assert InterpreterSupport.isEnabled(); + frameMap.reserveOutgoing(AArch64InterpreterStubs.additionalFrameSizeEnterStub()); + } else if (stubType == Deoptimizer.StubType.InterpreterLeaveStub) { + assert InterpreterSupport.isEnabled(); + frameMap.reserveOutgoing(AArch64InterpreterStubs.additionalFrameSizeLeaveStub()); + } + return lirGenerationResult; } @Override diff --git a/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/AMD64InterpreterStubs.java b/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/AMD64InterpreterStubs.java new file mode 100644 index 000000000000..dcbea077eefe --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/AMD64InterpreterStubs.java @@ -0,0 +1,648 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.graal.amd64; + +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.c.struct.OffsetOf; +import com.oracle.svm.core.config.ConfigurationValues; +import com.oracle.svm.core.deopt.DeoptimizationSlotPacking; +import com.oracle.svm.core.graal.code.InterpreterAccessStubData; +import com.oracle.svm.core.graal.meta.SubstrateRegisterConfig; +import com.oracle.svm.core.meta.SharedMethod; +import com.oracle.svm.core.util.VMError; +import jdk.graal.compiler.api.replacements.Fold; +import jdk.graal.compiler.asm.Label; +import jdk.graal.compiler.asm.amd64.AMD64Address; +import jdk.graal.compiler.asm.amd64.AMD64Assembler; +import jdk.graal.compiler.asm.amd64.AMD64MacroAssembler; +import jdk.graal.compiler.core.common.NumUtil; +import jdk.graal.compiler.lir.asm.CompilationResultBuilder; +import jdk.vm.ci.amd64.AMD64; +import jdk.vm.ci.code.CallingConvention; +import jdk.vm.ci.code.Register; +import jdk.vm.ci.code.RegisterArray; +import jdk.vm.ci.code.RegisterValue; +import jdk.vm.ci.code.StackSlot; +import jdk.vm.ci.meta.AllocatableValue; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.c.struct.RawField; +import org.graalvm.nativeimage.c.struct.RawStructure; +import org.graalvm.nativeimage.c.struct.SizeOf; +import org.graalvm.word.Pointer; +import org.graalvm.word.PointerBase; +import org.graalvm.word.WordFactory; + +import static jdk.vm.ci.amd64.AMD64.rax; +import static jdk.vm.ci.amd64.AMD64.rsp; +import static jdk.vm.ci.amd64.AMD64.xmm0; + +public class AMD64InterpreterStubs { + + private static SubstrateAMD64RegisterConfig getRegisterConfig() { + return new SubstrateAMD64RegisterConfig(SubstrateRegisterConfig.ConfigKind.NORMAL, null, ConfigurationValues.getTarget(), + SubstrateOptions.PreserveFramePointer.getValue()); + } + + public static final Register TRAMPOLINE_ARGUMENT = AMD64.rax; + + public static class InterpreterEnterStubContext extends SubstrateAMD64Backend.SubstrateAMD64FrameContext { + + public InterpreterEnterStubContext(SharedMethod method, CallingConvention callingConvention) { + super(method, callingConvention); + } + + private static AMD64Address createAddress(int offset) { + int deoptSlotSize = 8 + 8 /* padding */; + return new AMD64Address(rsp, deoptSlotSize + offset); + } + + @Override + public void enter(CompilationResultBuilder crb) { + AMD64MacroAssembler masm = (AMD64MacroAssembler) crb.asm; + + Register trampArg = TRAMPOLINE_ARGUMENT; + Register spCopy = AMD64.r11; + + masm.movq(spCopy, rsp); + + super.enter(crb); + + /* sp points to InterpreterData struct */ + masm.movq(createAddress(offsetAbiSpReg()), spCopy); + + RegisterArray gps = getRegisterConfig().getJavaGeneralParameterRegs(); + VMError.guarantee(gps.size() == 6); + + masm.movq(createAddress(offsetAbiGp0()), gps.get(0)); + masm.movq(createAddress(offsetAbiGp1()), gps.get(1)); + masm.movq(createAddress(offsetAbiGp2()), gps.get(2)); + masm.movq(createAddress(offsetAbiGp3()), gps.get(3)); + masm.movq(createAddress(offsetAbiGp4()), gps.get(4)); + masm.movq(createAddress(offsetAbiGp5()), gps.get(5)); + + RegisterArray fps = getRegisterConfig().getFloatingPointParameterRegs(); + + masm.movq(createAddress(offsetAbiFpArg0()), fps.get(0)); + masm.movq(createAddress(offsetAbiFpArg1()), fps.get(1)); + masm.movq(createAddress(offsetAbiFpArg2()), fps.get(2)); + masm.movq(createAddress(offsetAbiFpArg3()), fps.get(3)); + + if (Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class)) { + VMError.guarantee(fps.size() == 8); + masm.movq(createAddress(offsetAbiFpArg4()), fps.get(4)); + masm.movq(createAddress(offsetAbiFpArg5()), fps.get(5)); + masm.movq(createAddress(offsetAbiFpArg6()), fps.get(6)); + masm.movq(createAddress(offsetAbiFpArg7()), fps.get(7)); + } else { + assert Platform.includedIn(Platform.WINDOWS.class); + VMError.guarantee(fps.size() == 4); + } + + /* sp points to InterpreterData struct, move it as 2nd arg */ + masm.movq(gps.get(1), rsp); + masm.addq(gps.get(1), 16 /* deoptSlotSize */); + + /* Pass the interpreter method index as first arg */ + masm.movq(gps.get(0), trampArg); + } + + @Override + public void leave(CompilationResultBuilder crb) { + AMD64MacroAssembler masm = (AMD64MacroAssembler) crb.asm; + + /* rax is a pointer to InterpreterEnterData */ + + /* Move fp return value into ABI register */ + masm.movq(xmm0, new AMD64Address(rax, offsetAbiFpRet())); + + /* Move gp return value into ABI register */ + masm.movq(rax, new AMD64Address(rax, offsetAbiGpRet())); + + super.leave(crb); + } + } + + public static class InterpreterLeaveStubContext extends SubstrateAMD64Backend.SubstrateAMD64FrameContext { + + public InterpreterLeaveStubContext(SharedMethod method, CallingConvention callingConvention) { + super(method, callingConvention); + } + + @Override + public void enter(CompilationResultBuilder crb) { + super.enter(crb); + AMD64MacroAssembler masm = (AMD64MacroAssembler) crb.asm; + RegisterArray gps = getRegisterConfig().getJavaGeneralParameterRegs(); + + /* sp points to four reserved stack slots for this stub */ + + /* arg0 is untouched by this extra prolog */ + + /* arg1: Pointer to InterpreterData struct */ + masm.movq(new AMD64Address(rsp, 0), gps.get(1)); + /* arg2: Variable stack size */ + masm.movq(new AMD64Address(rsp, 8), gps.get(2)); + /* arg3: gcReferenceMap next */ + masm.movq(new AMD64Address(rsp, 16), gps.get(3)); + + /* 4th slot is for stack alignment to 0x10 */ + + masm.subq(rsp, gps.get(2) /* variable stack size */); + } + + @Override + public void leave(CompilationResultBuilder crb) { + AMD64MacroAssembler masm = (AMD64MacroAssembler) crb.asm; + RegisterArray gps = getRegisterConfig().getJavaGeneralParameterRegs(); + RegisterArray fps = getRegisterConfig().getFloatingPointParameterRegs(); + + /* Save call target */ + Register callTarget = AMD64.r10; + masm.movq(callTarget, rax); + + /* Get pointer to InterpreterData struct */ + masm.movq(rax, gps.get(1)); + + Register stackSize = AMD64.r11; + masm.movq(stackSize, gps.get(2)); + + Label regsHandling = new Label(); + /* if stackSize == 0 */ + masm.testq(stackSize, stackSize); + masm.jccb(AMD64Assembler.ConditionFlag.Zero, regsHandling); + + /* Copy prepared outgoing args to the stack where the ABI expects it */ + Register calleeSpArgs = AMD64.r12; + Register interpDataSp = AMD64.r13; + masm.movq(interpDataSp, new AMD64Address(rax, offsetAbiSpReg())); + masm.movq(calleeSpArgs, rsp); + + int wordSize = 8; + Label spCopyBegin = new Label(); + masm.bind(spCopyBegin); + /* 5th arg is not used, use it as a temp register */ + masm.movq(gps.get(4), new AMD64Address(interpDataSp, 0)); + masm.movq(new AMD64Address(calleeSpArgs, 0), gps.get(4)); + masm.addq(interpDataSp, wordSize); + masm.addq(calleeSpArgs, wordSize); + masm.subl(stackSize, wordSize); + + masm.testq(stackSize, stackSize); + masm.jccb(AMD64Assembler.ConditionFlag.NotZero, spCopyBegin); + + masm.bind(regsHandling); + + /* Set fp argument registers */ + masm.movq(fps.get(0), new AMD64Address(rax, offsetAbiFpArg0())); + masm.movq(fps.get(1), new AMD64Address(rax, offsetAbiFpArg1())); + masm.movq(fps.get(2), new AMD64Address(rax, offsetAbiFpArg2())); + masm.movq(fps.get(3), new AMD64Address(rax, offsetAbiFpArg3())); + + if (Platform.includedIn(Platform.LINUX.class) || Platform.includedIn(Platform.DARWIN.class)) { + masm.movq(fps.get(4), new AMD64Address(rax, offsetAbiFpArg4())); + masm.movq(fps.get(5), new AMD64Address(rax, offsetAbiFpArg5())); + masm.movq(fps.get(6), new AMD64Address(rax, offsetAbiFpArg6())); + masm.movq(fps.get(7), new AMD64Address(rax, offsetAbiFpArg7())); + } + + /* Set gp argument registers */ + masm.movq(gps.get(0), new AMD64Address(rax, offsetAbiGp0())); + masm.movq(gps.get(1), new AMD64Address(rax, offsetAbiGp1())); + masm.movq(gps.get(2), new AMD64Address(rax, offsetAbiGp2())); + masm.movq(gps.get(3), new AMD64Address(rax, offsetAbiGp3())); + masm.movq(gps.get(4), new AMD64Address(rax, offsetAbiGp4())); + masm.movq(gps.get(5), new AMD64Address(rax, offsetAbiGp5())); + + /* Call into target method */ + masm.call(callTarget); + + Register resultCopy = AMD64.r10; + masm.movq(resultCopy, rax); + + /* Obtain stack size from deopt slot */ + masm.movq(AMD64.r12, new AMD64Address(rsp, 0)); + + /* Assumption of deopt slot encoding */ + assert crb.target.stackAlignment == 0x10; + masm.shrq(AMD64.r12, DeoptimizationSlotPacking.POS_VARIABLE_FRAMESIZE - DeoptimizationSlotPacking.STACK_ALIGNMENT); + + /* Restore stack pointer */ + masm.addq(rsp, AMD64.r12); + + /* Pointer InterpreterData struct */ + masm.movq(rax, new AMD64Address(rsp, 0)); + + /* Save gp ABI register into InterpreterData struct */ + masm.movq(new AMD64Address(rax, offsetAbiGpRet()), resultCopy); + + /* Save fp ABI register into InterpreterData struct */ + masm.movq(new AMD64Address(rax, offsetAbiFpRet()), xmm0); + + super.leave(crb); + } + } + + public static int sizeOfInterpreterData() { + return NumUtil.roundUp(SizeOf.get(InterpreterDataAMD64.class), 0x10); + } + + public static int additionalFrameSizeEnterStub() { + int wordSize = 8; + int deoptSlotSize = wordSize + wordSize /* for padding */; + return sizeOfInterpreterData() + deoptSlotSize; + } + + public static int additionalFrameSizeLeaveStub() { + int wordSize = 8; + /* + * reserve four slots for: base address of outgoing stack args, variable stack size, + * gcReferenceMap, padding + */ + return 4 * wordSize; + } + + @RawStructure + public interface InterpreterDataAMD64 extends PointerBase { + @RawField + long getStackSize(); + + @RawField + void setStackSize(long val); + + @RawField + long getAbiSpReg(); + + @RawField + void setAbiSpReg(long val); + + /* arch specific */ + + @RawField + void setAbiGpRet(long val); + + @RawField + long getAbiGpRet(); + + @RawField + void setAbiFpRet(long val); + + @RawField + long getAbiFpRet(); + + @RawField + long getAbiGp0(); + + @RawField + void setAbiGp0(long val); + + @RawField + long getAbiGp1(); + + @RawField + void setAbiGp1(long val); + + @RawField + long getAbiGp2(); + + @RawField + void setAbiGp2(long val); + + @RawField + long getAbiGp3(); + + @RawField + void setAbiGp3(long val); + + @RawField + long getAbiGp4(); + + @RawField + void setAbiGp4(long val); + + @RawField + long getAbiGp5(); + + @RawField + void setAbiGp5(long val); + + @RawField + long getAbiFpArg0(); + + @RawField + void setAbiFpArg0(long val); + + @RawField + long getAbiFpArg1(); + + @RawField + void setAbiFpArg1(long val); + + @RawField + long getAbiFpArg2(); + + @RawField + void setAbiFpArg2(long val); + + @RawField + long getAbiFpArg3(); + + @RawField + void setAbiFpArg3(long val); + + // GR-55154: We could save 32 bytes on Windows by exluding storage for xmm4-xmm7 + @RawField + long getAbiFpArg4(); + + @RawField + void setAbiFpArg4(long val); + + @RawField + long getAbiFpArg5(); + + @RawField + void setAbiFpArg5(long val); + + @RawField + long getAbiFpArg6(); + + @RawField + void setAbiFpArg6(long val); + + @RawField + long getAbiFpArg7(); + + @RawField + void setAbiFpArg7(long val); + } + + @Fold + public static int offsetAbiSpReg() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiSpReg"); + } + + @Fold + public static int offsetAbiGp0() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiGp0"); + } + + @Fold + public static int offsetAbiGp1() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiGp1"); + } + + @Fold + public static int offsetAbiGp2() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiGp2"); + } + + @Fold + public static int offsetAbiGp3() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiGp3"); + } + + @Fold + public static int offsetAbiGp4() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiGp4"); + } + + @Fold + public static int offsetAbiGp5() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiGp5"); + } + + @Fold + public static int offsetAbiFpArg0() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpArg0"); + } + + @Fold + public static int offsetAbiFpArg1() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpArg1"); + } + + @Fold + public static int offsetAbiFpArg2() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpArg2"); + } + + @Fold + public static int offsetAbiFpArg3() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpArg3"); + } + + @Fold + public static int offsetAbiFpArg4() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpArg4"); + } + + @Fold + public static int offsetAbiFpArg5() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpArg5"); + } + + @Fold + public static int offsetAbiFpArg6() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpArg6"); + } + + @Fold + public static int offsetAbiFpArg7() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpArg7"); + } + + @Fold + public static int offsetAbiGpRet() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiGpRet"); + } + + @Fold + public static int offsetAbiFpRet() { + return OffsetOf.get(InterpreterDataAMD64.class, "AbiFpRet"); + } + + public static class AMD64InterpreterAccessStubData implements InterpreterAccessStubData { + + @Override + public void setSp(Pointer data, int stackSize, Pointer stackBuffer) { + VMError.guarantee(stackBuffer.isNonNull()); + + InterpreterDataAMD64 p = (InterpreterDataAMD64) data; + p.setAbiSpReg(stackBuffer.rawValue()); + p.setStackSize(stackSize); + + /* + * We re-use the deopt slot to leave the stack size at a known place for the stack + * walker. See + * + * com.oracle.svm.core.graal.amd64.SubstrateAMD64RegisterConfig#getCallingConvention + * + * comment above `currentStackOffset` declaration. + */ + assert stackSize % 0x10 == 0; + stackBuffer.writeLong(0, DeoptimizationSlotPacking.encodeVariableFrameSizeIntoDeoptSlot(stackSize)); + } + + @Override + public long getGpArgumentAt(AllocatableValue ccArg, Pointer data, int pos) { + InterpreterDataAMD64 p = (InterpreterDataAMD64) data; + return switch (pos) { + case 0 -> p.getAbiGp0(); + case 1 -> p.getAbiGp1(); + case 2 -> p.getAbiGp2(); + case 3 -> p.getAbiGp3(); + case 4 -> p.getAbiGp4(); + case 5 -> p.getAbiGp5(); + default -> { + StackSlot stackSlot = (StackSlot) ccArg; + Pointer sp = WordFactory.pointer(p.getAbiSpReg()); + int spAdjustmentOnCall = ConfigurationValues.getTarget().wordSize; + int offset = stackSlot.getOffset(0) + spAdjustmentOnCall; + yield sp.readLong(offset); + } + }; + } + + @Override + public long setGpArgumentAt(AllocatableValue ccArg, Pointer data, int pos, long val) { + InterpreterDataAMD64 p = (InterpreterDataAMD64) data; + if (pos >= 0 && pos <= 5) { + VMError.guarantee(ccArg instanceof RegisterValue); + switch (pos) { + case 0 -> p.setAbiGp0(val); + case 1 -> p.setAbiGp1(val); + case 2 -> p.setAbiGp2(val); + case 3 -> p.setAbiGp3(val); + case 4 -> p.setAbiGp4(val); + case 5 -> p.setAbiGp5(val); + } + /* no GC mask required */ + return 0; + } + StackSlot stackSlot = (StackSlot) ccArg; + + Pointer sp = WordFactory.pointer(p.getAbiSpReg()); + int offset = stackSlot.getOffset(0); + VMError.guarantee(sp.isNonNull()); + VMError.guarantee(offset < p.getStackSize()); + + sp.writeLong(offset, val); + + VMError.guarantee((pos - 6) < Long.SIZE, "more than 64 stack args are not supported"); + return 1L << (pos - 6); + } + + private static int upperFpEnd() { + /* only 4 floating point regs on Windows, 8 otherwise */ + return Platform.includedIn(Platform.WINDOWS.class) ? 3 : 7; + } + + @Override + public long getFpArgumentAt(AllocatableValue ccArg, Pointer data, int pos) { + InterpreterDataAMD64 p = (InterpreterDataAMD64) data; + if (pos >= 0 && pos <= upperFpEnd()) { + VMError.guarantee(ccArg instanceof RegisterValue); + switch (pos) { + case 0: + return p.getAbiFpArg0(); + case 1: + return p.getAbiFpArg1(); + case 2: + return p.getAbiFpArg2(); + case 3: + return p.getAbiFpArg3(); + case 4: + return p.getAbiFpArg4(); + case 5: + return p.getAbiFpArg5(); + case 6: + return p.getAbiFpArg6(); + case 7: + return p.getAbiFpArg7(); + } + } + StackSlot stackSlot = (StackSlot) ccArg; + Pointer sp = WordFactory.pointer(p.getAbiSpReg()); + + int spAdjustmentOnCall = ConfigurationValues.getTarget().wordSize; + int offset = stackSlot.getOffset(0) + spAdjustmentOnCall; + + return sp.readLong(offset); + } + + @Override + public void setFpArgumentAt(AllocatableValue ccArg, Pointer data, int pos, long val) { + InterpreterDataAMD64 p = (InterpreterDataAMD64) data; + if (pos >= 0 && pos <= upperFpEnd()) { + VMError.guarantee(ccArg instanceof RegisterValue); + switch (pos) { + case 0 -> p.setAbiFpArg0(val); + case 1 -> p.setAbiFpArg1(val); + case 2 -> p.setAbiFpArg2(val); + case 3 -> p.setAbiFpArg3(val); + case 4 -> p.setAbiFpArg4(val); + case 5 -> p.setAbiFpArg5(val); + case 6 -> p.setAbiFpArg6(val); + case 7 -> p.setAbiFpArg7(val); + } + } else { + StackSlot stackSlot = (StackSlot) ccArg; + + Pointer sp = WordFactory.pointer(p.getAbiSpReg()); + int offset = stackSlot.getOffset(0); + + VMError.guarantee(sp.isNonNull()); + VMError.guarantee(offset < p.getStackSize()); + + sp.writeLong(offset, val); + } + } + + @Override + public long getGpReturn(Pointer data) { + return ((InterpreterDataAMD64) data).getAbiGpRet(); + } + + @Override + public void setGpReturn(Pointer data, long gpReturn) { + ((InterpreterDataAMD64) data).setAbiGpRet(gpReturn); + } + + @Override + public long getFpReturn(Pointer data) { + return ((InterpreterDataAMD64) data).getAbiFpRet(); + } + + @Override + public void setFpReturn(Pointer data, long fpReturn) { + ((InterpreterDataAMD64) data).setAbiFpRet(fpReturn); + } + + @Override + @Fold + public int allocateStubDataSize() { + return sizeOfInterpreterData(); + } + } +} diff --git a/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/SubstrateAMD64Backend.java b/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/SubstrateAMD64Backend.java index 2c42cbd0803b..7e330886f8f6 100644 --- a/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/SubstrateAMD64Backend.java +++ b/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/SubstrateAMD64Backend.java @@ -46,6 +46,7 @@ import java.util.EnumSet; import java.util.function.BiConsumer; +import com.oracle.svm.core.interpreter.InterpreterSupport; import org.graalvm.collections.EconomicMap; import org.graalvm.nativeimage.ImageSingletons; @@ -1705,10 +1706,24 @@ private static boolean isCalleeSaved(Register register, RegisterConfig config, S @Override public LIRGenerationResult newLIRGenerationResult(CompilationIdentifier compilationId, LIR lir, RegisterAllocationConfig registerAllocationConfig, StructuredGraph graph, Object stub) { SharedMethod method = (SharedMethod) graph.method(); + SubstrateCallingConventionKind ccKind = method.getCallingConventionKind(); SubstrateCallingConventionType ccType = ccKind.isCustom() ? method.getCustomCallingConventionType() : ccKind.toType(false); CallingConvention callingConvention = CodeUtil.getCallingConvention(getCodeCache(), ccType, method, this); - return new SubstrateLIRGenerationResult(compilationId, lir, newFrameMapBuilder(registerAllocationConfig.getRegisterConfig(), method), callingConvention, registerAllocationConfig, method); + LIRGenerationResult lirGenerationResult = new SubstrateLIRGenerationResult(compilationId, lir, newFrameMapBuilder(registerAllocationConfig.getRegisterConfig(), method), callingConvention, + registerAllocationConfig, method); + + FrameMap frameMap = ((FrameMapBuilderTool) lirGenerationResult.getFrameMapBuilder()).getFrameMap(); + Deoptimizer.StubType stubType = method.getDeoptStubType(); + if (stubType == Deoptimizer.StubType.InterpreterEnterStub) { + assert InterpreterSupport.isEnabled(); + frameMap.reserveOutgoing(AMD64InterpreterStubs.additionalFrameSizeEnterStub()); + } else if (stubType == Deoptimizer.StubType.InterpreterLeaveStub) { + assert InterpreterSupport.isEnabled(); + frameMap.reserveOutgoing(AMD64InterpreterStubs.additionalFrameSizeLeaveStub()); + } + + return lirGenerationResult; } protected AMD64ArithmeticLIRGenerator createArithmeticLIRGen(RegisterValue nullRegisterValue) { @@ -1799,7 +1814,14 @@ protected FrameContext createFrameContext(SharedMethod method, Deoptimizer.StubT return new DeoptEntryStubContext(method, callingConvention); } else if (stubType == Deoptimizer.StubType.ExitStub) { return new DeoptExitStubContext(method, callingConvention); + } else if (stubType == Deoptimizer.StubType.InterpreterEnterStub) { + assert InterpreterSupport.isEnabled(); + return new AMD64InterpreterStubs.InterpreterEnterStubContext(method, callingConvention); + } else if (stubType == Deoptimizer.StubType.InterpreterLeaveStub) { + assert InterpreterSupport.isEnabled(); + return new AMD64InterpreterStubs.InterpreterLeaveStubContext(method, callingConvention); } + return new SubstrateAMD64FrameContext(method, callingConvention); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/deopt/Deoptimizer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/deopt/Deoptimizer.java index 2dbecf6ff31c..e5fba28dde02 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/deopt/Deoptimizer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/deopt/Deoptimizer.java @@ -545,7 +545,11 @@ public enum StubType { *

* Custom epilogue: prepare stack layout and ABI registers for outgoing call. */ - InterpreterLeaveStub + InterpreterLeaveStub; + + public boolean isInterpreterStub() { + return equals(InterpreterEnterStub) || equals(InterpreterLeaveStub); + } } @Retention(RetentionPolicy.RUNTIME) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/code/InterpreterAccessStubData.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/code/InterpreterAccessStubData.java new file mode 100644 index 000000000000..8d0b2d5a8e9c --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/code/InterpreterAccessStubData.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.graal.code; + +import jdk.vm.ci.meta.AllocatableValue; +import org.graalvm.word.Pointer; + +/* Helper class to set ABI specific data */ +public interface InterpreterAccessStubData { + void setSp(Pointer data, int stackSize, Pointer stackBuffer); + + long getGpArgumentAt(AllocatableValue ccArg, Pointer data, int pos); + + long setGpArgumentAt(AllocatableValue ccArg, Pointer data, int pos, long val); + + long getFpArgumentAt(AllocatableValue ccArg, Pointer data, int pos); + + void setFpArgumentAt(AllocatableValue ccArg, Pointer data, int pos, long val); + + long getGpReturn(Pointer data); + + void setGpReturn(Pointer data, long gpReturn); + + long getFpReturn(Pointer data); + + void setFpReturn(Pointer data, long fpReturn); + + int allocateStubDataSize(); +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/ByteUtils.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/ByteUtils.java new file mode 100644 index 000000000000..7e6c6a87ecc6 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/ByteUtils.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter.metadata; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; + +/** + * A collection of utility methods for dealing with bytes, particularly in byte arrays. + */ +public final class ByteUtils { + + private static final VarHandle BYTE_ARRAY_VARHANDLE = MethodHandles.arrayElementVarHandle(byte[].class); + + /** + * Gets a signed 1-byte value. + * + * @param data the array containing the data + * @param bci the start index of the value to retrieve + * @return the signed 1-byte value at index {@code bci} in array {@code data} + */ + public static int beS1(byte[] data, int bci) { + return data[bci]; + } + + /** + * Gets a signed 2-byte big-endian value. + * + * @param data the array containing the data + * @param bci the start index of the value to retrieve + * @return the signed 2-byte, big-endian, value at index {@code bci} in array {@code data} + */ + public static int beS2(byte[] data, int bci) { + return (data[bci] << 8) | (data[bci + 1] & 0xff); + } + + /** + * Gets an unsigned 1-byte value. + * + * @param data the array containing the data + * @param bci the start index of the value to retrieve + * @return the unsigned 1-byte value at index {@code bci} in array {@code data} + */ + public static int beU1(byte[] data, int bci) { + return data[bci] & 0xff; + } + + /** + * Gets an unsigned 1-byte value in a volatile fashion. + * + * @param data the array containing the data + * @param bci the start index of the value to retrieve + * @return the unsigned 1-byte value at index {@code bci} in array {@code data} + */ + public static int volatileBeU1(byte[] data, int bci) { + return ((byte) BYTE_ARRAY_VARHANDLE.getVolatile(data, bci)) & 0xff; + } + + /** + * Gets an unsigned 1-byte value in, with opaque semantics. + * + * @param data the array containing the data + * @param bci the start index of the value to retrieve + * @return the unsigned 1-byte value at index {@code bci} in array {@code data} + */ + public static int opaqueBeU1(byte[] data, int bci) { + return ((byte) BYTE_ARRAY_VARHANDLE.getOpaque(data, bci)) & 0xff; + } + + /** + * Gets an unsigned 2-byte big-endian value. + * + * @param data the array containing the data + * @param bci the start index of the value to retrieve + * @return the unsigned 2-byte, big-endian, value at index {@code bci} in array {@code data} + */ + public static int beU2(byte[] data, int bci) { + return ((data[bci] & 0xff) << 8) | (data[bci + 1] & 0xff); + } + + /** + * Gets a signed 4-byte big-endian value. + * + * @param data the array containing the data + * @param bci the start index of the value to retrieve + * @return the signed 4-byte, big-endian, value at index {@code bci} in array {@code data} + */ + public static int beS4(byte[] data, int bci) { + return (data[bci] << 24) | ((data[bci + 1] & 0xff) << 16) | ((data[bci + 2] & 0xff) << 8) | (data[bci + 3] & 0xff); + } + + /** + * Gets either a signed 2-byte or a signed 4-byte big-endian value. + * + * @param data the array containing the data + * @param bci the start index of the value to retrieve + * @param fourByte if true, this method will return a 4-byte value + * @return the signed 2 or 4-byte, big-endian, value at index {@code bci} in array {@code data} + */ + public static int beSVar(byte[] data, int bci, boolean fourByte) { + if (fourByte) { + return beS4(data, bci); + } else { + return beS2(data, bci); + } + } + + public static void opaqueWrite(byte[] array, int index, byte value) { + BYTE_ARRAY_VARHANDLE.setOpaque(array, index, value); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/BytecodeStream.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/BytecodeStream.java new file mode 100644 index 000000000000..cfb888790ab9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/BytecodeStream.java @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter.metadata; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.ANEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.CHECKCAST; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GETFIELD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GETSTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INSTANCEOF; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEDYNAMIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEINTERFACE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESPECIAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEVIRTUAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC2_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.MULTIANEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.NEW; +import static com.oracle.svm.interpreter.metadata.Bytecodes.PUTFIELD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.PUTSTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.WIDE; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.VMError; + +/** + * A utility class that makes iterating over bytecodes and reading operands simpler and less + * error-prone. For example, it handles the {@link Bytecodes#WIDE} instruction and wide variants of + * instructions internally. + * + * Some accessors have a suffix indicating the type of bytecode it handles, these do NOT + * handle the {@link Bytecodes#WIDE} modifier. Methods without the numeric suffix will handle their + * {@link Bytecodes#WIDE} modifier internally, but may be slower. + */ +public final class BytecodeStream { + + private BytecodeStream() { + throw VMError.shouldNotReachHere("private constructor"); + } + + /** + * Gets the next bytecode index (no side-effects). + * + * @return the next bytecode index + */ + public static int nextBCI(byte[] code, int curBCI) { + return curBCI + lengthOf(code, curBCI); + } + + /** + * Gets the bytecode index of the end of the code. + * + * @return the index of the end of the code + */ + public static int endBCI(byte[] code) { + return code.length; + } + + /** + * Gets the current opcode. This method will never return the {@link Bytecodes#WIDE WIDE} + * opcode, but will instead return the opcode that is modified by the {@code WIDE} opcode. + * + * @return the current opcode; + * @see #opcode(byte[], int) + */ + public static int currentBC(byte[] code, int curBCI) { + int opcode = opcode(code, curBCI); + if (opcode == WIDE) { + return ByteUtils.beU1(code, curBCI + 1); + } else { + return opcode; + } + } + + /** + * Gets the current opcode in a volatile fashion. This method will never return the + * {@link Bytecodes#WIDE WIDE} opcode, but will instead return the opcode that is modified by + * the {@code WIDE} opcode. + * + * @return the current opcode; {@link Bytecodes#END} if at or beyond the end of the code + */ + public static int currentVolatileBC(byte[] code, int curBCI) { + int opcode = volatileOpcode(code, curBCI); + if (opcode == WIDE) { + return ByteUtils.volatileBeU1(code, curBCI + 1); + } else { + return opcode; + } + } + + /** + * Reads the index of a local variable for one of the load or store instructions. The WIDE + * modifier is handled internally. + * + * @return the index of the local variable + */ + public static int readLocalIndex(byte[] code, int curBCI) { + // read local variable index for load/store + if (opcode(code, curBCI) == WIDE) { + return ByteUtils.beU2(code, curBCI + 2); + } + return ByteUtils.beU1(code, curBCI + 1); + } + + /** + * Reads the index of a local variable for one of the load or store instructions. The + * {@link Bytecodes#WIDE} modifier is handled internally. + * + * @return the index of the local variable + */ + public static int readLocalIndex1(byte[] code, int curBCI) { + // read local variable index for load/store + return ByteUtils.beU1(code, curBCI + 1); + } + + /** + * Reads the index of a local variable for one of the load or store instructions. The + * {@link Bytecodes#WIDE} modifier is NOT handled internally. + * + * @return the index of the local variable + */ + public static int readLocalIndex2(byte[] code, int curBCI) { + // read local variable index for load/store + return ByteUtils.beU2(code, curBCI + 2); + } + + /** + * Read the delta for an {@link Bytecodes#IINC} bytecode. The {@link Bytecodes#WIDE} is handled. + * + * @return the delta for the {@code IINC} + */ + public static int readIncrement(byte[] code, int curBCI) { + // read the delta for the iinc bytecode + if (opcode(code, curBCI) == WIDE) { + return ByteUtils.beS2(code, curBCI + 4); + } + return ByteUtils.beS1(code, curBCI + 2); + } + + /** + * Read the delta for an {@link Bytecodes#IINC} bytecode. The {@link Bytecodes#WIDE} modifier is + * NOThandled internally. + * + * @return the delta for the {@code IINC} + */ + public static int readIncrement1(byte[] code, int curBCI) { + // read the delta for the iinc bytecode + return ByteUtils.beS1(code, curBCI + 2); + } + + /** + * Read the delta for a {@link Bytecodes#WIDE} + {@link Bytecodes#IINC} bytecode. + * + * @return the delta for the {@code WIDE IINC} + */ + public static int readIncrement2(byte[] code, int curBCI) { + // read the delta for the iinc bytecode + return ByteUtils.beS2(code, curBCI + 4); + } + + /** + * Read the destination of a {@link Bytecodes#GOTO} or {@code IF} instructions. Wide bytecodes: + * {@link Bytecodes#GOTO_W} {@link Bytecodes#JSR_W}, are handled internally. + * + * @return the destination bytecode index + */ + public static int readBranchDest(byte[] code, int curBCI) { + // reads the destination for a branch bytecode + int opcode = opcode(code, curBCI); + if (opcode == Bytecodes.GOTO_W || opcode == Bytecodes.JSR_W) { + return curBCI + ByteUtils.beS4(code, curBCI + 1); + } else { + return curBCI + ByteUtils.beS2(code, curBCI + 1); + } + } + + /** + * Read the destination of a {@link Bytecodes#GOTO_W} or {@code JSR_W} instructions. + * + * @return the destination bytecode index + */ + public static int readBranchDest4(byte[] code, int curBCI) { + // reads the destination for a branch bytecode + return curBCI + ByteUtils.beS4(code, curBCI + 1); + } + + /** + * Read the destination of a {@link Bytecodes#GOTO} or {@code IF} instructions. + * + * @return the destination bytecode index + */ + public static int readBranchDest2(byte[] code, int curBCI) { + // reads the destination for a branch bytecode + return curBCI + ByteUtils.beS2(code, curBCI + 1); + } + + /** + * Read a signed 4-byte integer from the bytecode stream at the specified bytecode index. + * + * @param bci the bytecode index + * @return the integer value + */ + public static int readInt(byte[] code, int bci) { + // reads a 4-byte signed value + return ByteUtils.beS4(code, bci); + } + + /** + * Reads an unsigned, 1-byte value from the bytecode stream at the specified bytecode index. + * + * @param bci the bytecode index + * @return the byte + */ + public static int readUByte(byte[] code, int bci) { + return ByteUtils.beU1(code, bci); + } + + /** + * Reads a 1-byte constant pool index for the current instruction. Used by + * {@link Bytecodes#LDC}. + * + * @return the constant pool index + */ + public static char readCPI1(byte[] code, int curBCI) { + return (char) ByteUtils.beU1(code, curBCI + 1); + } + + /** + * Reads a 2-byte constant pool index for the current instruction. + * + * @return the constant pool index + */ + public static char readCPI2(byte[] code, int curBCI) { + return (char) ByteUtils.beU2(code, curBCI + 1); + } + + /** + * Reads a constant pool index for the current instruction. + * + * @return the constant pool index + */ + public static char readCPI(byte[] code, int curBCI) { + if (opcode(code, curBCI) == LDC) { + return (char) ByteUtils.beU1(code, curBCI + 1); + } + return (char) ByteUtils.beU2(code, curBCI + 1); + } + + /** + * Reads a constant pool index for an invokedynamic instruction. + * + * @return the constant pool index + */ + public static int readCPI4(byte[] code, int curBCI) { + assert opcode(code, curBCI) == Bytecodes.INVOKEDYNAMIC; + return ByteUtils.beS4(code, curBCI + 1); + } + + /** + * Reads a signed, 1-byte value for the current instruction (e.g. BIPUSH). + * + * @return the byte + */ + public static byte readByte(byte[] code, int curBCI) { + return code[curBCI + 1]; + } + + /** + * Reads a signed, 2-byte short for the current instruction (e.g. SIPUSH). + * + * @return the short value + */ + public static short readShort(byte[] code, int curBCI) { + return (short) ByteUtils.beS2(code, curBCI + 1); + } + + /** + * Reads an unsigned byte for the current instruction (e.g. SIPUSH). The {@link Bytecodes#WIDE} + * modifier is NOT handled internally. + * + * @return the short value + */ + public static int opcode(byte[] code, int curBCI) { + // opcode validity is performed at verification time. + return ByteUtils.beU1(code, curBCI); + } + + /** + * Reads an unsigned byte for the current instruction (e.g. SIPUSH). The {@link Bytecodes#WIDE} + * modifier is NOT handled internally. It performs an opaque memory access. + */ + public static int opaqueOpcode(byte[] code, int curBCI) { + // opcode validity is performed at verification time. + return ByteUtils.opaqueBeU1(code, curBCI); + } + + public static int volatileOpcode(byte[] code, int curBCI) { + // opcode validity is performed at verification time. + return ByteUtils.volatileBeU1(code, curBCI); + } + + /** + * Gets the length of the current bytecode. It takes into account bytecodes with non-constant + * size and the {@link Bytecodes#WIDE} bytecode. + */ + private static int lengthOf(byte[] code, int curBCI) { + int opcode = opcode(code, curBCI); + int length = Bytecodes.lengthOf(opcode); + if (length == 0) { + switch (opcode) { + case Bytecodes.TABLESWITCH: { + return TableSwitch.size(code, curBCI); + } + case Bytecodes.LOOKUPSWITCH: { + return LookupSwitch.size(code, curBCI); + } + case WIDE: { + int opc = ByteUtils.beU1(code, curBCI + 1); + if (opc == Bytecodes.IINC) { + return 6; + } else { + return 4; // a load or store bytecode + } + } + default: + // Should rather be CompilerAsserts.neverPartOfCompilation() but this is + // reachable in SVM. + throw VMError.shouldNotReachHere(unknownVariableLengthBytecodeMessage(opcode)); + } + } + return length; + } + + private static String unknownVariableLengthBytecodeMessage(int opcode) { + return "unknown variable-length bytecode: " + opcode; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static void patchAppendixCPI(byte[] code, int curBCI, int appendixCPI) { + int opcode = opcode(code, curBCI); + switch (opcode) { + case INVOKEDYNAMIC: + code[curBCI + 3] = (byte) ((appendixCPI >> 8) & 0xFF); + code[curBCI + 4] = (byte) (appendixCPI & 0xFF); + break; + default: + throw VMError.shouldNotReachHereAtRuntime(); + } + } + + /** + * No CPI patching at runtime. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public static void patchCPI(byte[] code, int curBCI, int newCPI) { + int opcode = opcode(code, curBCI); + switch (opcode) { + case WIDE: + throw VMError.intentionallyUnimplemented(); + case LDC: { + // Ensure LDC CPI fits in a byte. + VMError.guarantee(0 <= newCPI && newCPI <= 0xFF); + code[curBCI + 1] = (byte) newCPI; + break; + } + // @formatter:off + case INVOKEVIRTUAL: // fall-through + case INVOKEINTERFACE:// fall-through + case INVOKESTATIC: // fall-through + case INVOKESPECIAL: // fall-through + case INVOKEDYNAMIC: // fall-through + + case PUTFIELD: // fall-through + case PUTSTATIC: // fall-through + + case GETFIELD: // fall-through + case GETSTATIC: // fall-through + case LDC2_W: // fall-through + case LDC_W: // fall-through + case ANEWARRAY: // fall-through + case NEW: // fall-through + case MULTIANEWARRAY: // fall-through + case INSTANCEOF: // fall-through + case CHECKCAST: { + VMError.guarantee(0 <= newCPI && newCPI <= 0xFFFF); + code[curBCI + 1] = (byte) ((newCPI >> 8) & 0xFF); + code[curBCI + 2] = (byte) (newCPI & 0xFF); + break; + } + // @formatter:on + default: + throw VMError.intentionallyUnimplemented(); + } + } + + /** + * Updates the specified array with opaque memory order semantics. + */ + public static void patchOpcodeOpaque(byte[] code, int curBCI, int newOpcode) { + assert 0 <= newOpcode && newOpcode < Bytecodes.END; + ByteUtils.opaqueWrite(code, curBCI, (byte) newOpcode); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/Bytecodes.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/Bytecodes.java new file mode 100644 index 000000000000..dcab28120e7a --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/Bytecodes.java @@ -0,0 +1,760 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter.metadata; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.ASSOCIATIVE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.BRANCH; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.COMMUTATIVE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.FALL_THROUGH; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.FIELD_READ; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.FIELD_WRITE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.INVOKE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.LOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.STOP; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.STORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.Flags.TRAP; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +/** + * Definitions of the standard Java bytecodes defined by + * Java + * Virtual Machine Specification. + */ +public class Bytecodes { + + // @formatter:off + public static final int NOP = 0; // 0x00 + public static final int ACONST_NULL = 1; // 0x01 + public static final int ICONST_M1 = 2; // 0x02 + public static final int ICONST_0 = 3; // 0x03 + public static final int ICONST_1 = 4; // 0x04 + public static final int ICONST_2 = 5; // 0x05 + public static final int ICONST_3 = 6; // 0x06 + public static final int ICONST_4 = 7; // 0x07 + public static final int ICONST_5 = 8; // 0x08 + public static final int LCONST_0 = 9; // 0x09 + public static final int LCONST_1 = 10; // 0x0A + public static final int FCONST_0 = 11; // 0x0B + public static final int FCONST_1 = 12; // 0x0C + public static final int FCONST_2 = 13; // 0x0D + public static final int DCONST_0 = 14; // 0x0E + public static final int DCONST_1 = 15; // 0x0F + public static final int BIPUSH = 16; // 0x10 + public static final int SIPUSH = 17; // 0x11 + public static final int LDC = 18; // 0x12 + public static final int LDC_W = 19; // 0x13 + public static final int LDC2_W = 20; // 0x14 + public static final int ILOAD = 21; // 0x15 + public static final int LLOAD = 22; // 0x16 + public static final int FLOAD = 23; // 0x17 + public static final int DLOAD = 24; // 0x18 + public static final int ALOAD = 25; // 0x19 + public static final int ILOAD_0 = 26; // 0x1A + public static final int ILOAD_1 = 27; // 0x1B + public static final int ILOAD_2 = 28; // 0x1C + public static final int ILOAD_3 = 29; // 0x1D + public static final int LLOAD_0 = 30; // 0x1E + public static final int LLOAD_1 = 31; // 0x1F + public static final int LLOAD_2 = 32; // 0x20 + public static final int LLOAD_3 = 33; // 0x21 + public static final int FLOAD_0 = 34; // 0x22 + public static final int FLOAD_1 = 35; // 0x23 + public static final int FLOAD_2 = 36; // 0x24 + public static final int FLOAD_3 = 37; // 0x25 + public static final int DLOAD_0 = 38; // 0x26 + public static final int DLOAD_1 = 39; // 0x27 + public static final int DLOAD_2 = 40; // 0x28 + public static final int DLOAD_3 = 41; // 0x29 + public static final int ALOAD_0 = 42; // 0x2A + public static final int ALOAD_1 = 43; // 0x2B + public static final int ALOAD_2 = 44; // 0x2C + public static final int ALOAD_3 = 45; // 0x2D + public static final int IALOAD = 46; // 0x2E + public static final int LALOAD = 47; // 0x2F + public static final int FALOAD = 48; // 0x30 + public static final int DALOAD = 49; // 0x31 + public static final int AALOAD = 50; // 0x32 + public static final int BALOAD = 51; // 0x33 + public static final int CALOAD = 52; // 0x34 + public static final int SALOAD = 53; // 0x35 + public static final int ISTORE = 54; // 0x36 + public static final int LSTORE = 55; // 0x37 + public static final int FSTORE = 56; // 0x38 + public static final int DSTORE = 57; // 0x39 + public static final int ASTORE = 58; // 0x3A + public static final int ISTORE_0 = 59; // 0x3B + public static final int ISTORE_1 = 60; // 0x3C + public static final int ISTORE_2 = 61; // 0x3D + public static final int ISTORE_3 = 62; // 0x3E + public static final int LSTORE_0 = 63; // 0x3F + public static final int LSTORE_1 = 64; // 0x40 + public static final int LSTORE_2 = 65; // 0x41 + public static final int LSTORE_3 = 66; // 0x42 + public static final int FSTORE_0 = 67; // 0x43 + public static final int FSTORE_1 = 68; // 0x44 + public static final int FSTORE_2 = 69; // 0x45 + public static final int FSTORE_3 = 70; // 0x46 + public static final int DSTORE_0 = 71; // 0x47 + public static final int DSTORE_1 = 72; // 0x48 + public static final int DSTORE_2 = 73; // 0x49 + public static final int DSTORE_3 = 74; // 0x4A + public static final int ASTORE_0 = 75; // 0x4B + public static final int ASTORE_1 = 76; // 0x4C + public static final int ASTORE_2 = 77; // 0x4D + public static final int ASTORE_3 = 78; // 0x4E + public static final int IASTORE = 79; // 0x4F + public static final int LASTORE = 80; // 0x50 + public static final int FASTORE = 81; // 0x51 + public static final int DASTORE = 82; // 0x52 + public static final int AASTORE = 83; // 0x53 + public static final int BASTORE = 84; // 0x54 + public static final int CASTORE = 85; // 0x55 + public static final int SASTORE = 86; // 0x56 + public static final int POP = 87; // 0x57 + public static final int POP2 = 88; // 0x58 + public static final int DUP = 89; // 0x59 + public static final int DUP_X1 = 90; // 0x5A + public static final int DUP_X2 = 91; // 0x5B + public static final int DUP2 = 92; // 0x5C + public static final int DUP2_X1 = 93; // 0x5D + public static final int DUP2_X2 = 94; // 0x5E + public static final int SWAP = 95; // 0x5F + public static final int IADD = 96; // 0x60 + public static final int LADD = 97; // 0x61 + public static final int FADD = 98; // 0x62 + public static final int DADD = 99; // 0x63 + public static final int ISUB = 100; // 0x64 + public static final int LSUB = 101; // 0x65 + public static final int FSUB = 102; // 0x66 + public static final int DSUB = 103; // 0x67 + public static final int IMUL = 104; // 0x68 + public static final int LMUL = 105; // 0x69 + public static final int FMUL = 106; // 0x6A + public static final int DMUL = 107; // 0x6B + public static final int IDIV = 108; // 0x6C + public static final int LDIV = 109; // 0x6D + public static final int FDIV = 110; // 0x6E + public static final int DDIV = 111; // 0x6F + public static final int IREM = 112; // 0x70 + public static final int LREM = 113; // 0x71 + public static final int FREM = 114; // 0x72 + public static final int DREM = 115; // 0x73 + public static final int INEG = 116; // 0x74 + public static final int LNEG = 117; // 0x75 + public static final int FNEG = 118; // 0x76 + public static final int DNEG = 119; // 0x77 + public static final int ISHL = 120; // 0x78 + public static final int LSHL = 121; // 0x79 + public static final int ISHR = 122; // 0x7A + public static final int LSHR = 123; // 0x7B + public static final int IUSHR = 124; // 0x7C + public static final int LUSHR = 125; // 0x7D + public static final int IAND = 126; // 0x7E + public static final int LAND = 127; // 0x7F + public static final int IOR = 128; // 0x80 + public static final int LOR = 129; // 0x81 + public static final int IXOR = 130; // 0x82 + public static final int LXOR = 131; // 0x83 + public static final int IINC = 132; // 0x84 + public static final int I2L = 133; // 0x85 + public static final int I2F = 134; // 0x86 + public static final int I2D = 135; // 0x87 + public static final int L2I = 136; // 0x88 + public static final int L2F = 137; // 0x89 + public static final int L2D = 138; // 0x8A + public static final int F2I = 139; // 0x8B + public static final int F2L = 140; // 0x8C + public static final int F2D = 141; // 0x8D + public static final int D2I = 142; // 0x8E + public static final int D2L = 143; // 0x8F + public static final int D2F = 144; // 0x90 + public static final int I2B = 145; // 0x91 + public static final int I2C = 146; // 0x92 + public static final int I2S = 147; // 0x93 + public static final int LCMP = 148; // 0x94 + public static final int FCMPL = 149; // 0x95 + public static final int FCMPG = 150; // 0x96 + public static final int DCMPL = 151; // 0x97 + public static final int DCMPG = 152; // 0x98 + public static final int IFEQ = 153; // 0x99 + public static final int IFNE = 154; // 0x9A + public static final int IFLT = 155; // 0x9B + public static final int IFGE = 156; // 0x9C + public static final int IFGT = 157; // 0x9D + public static final int IFLE = 158; // 0x9E + public static final int IF_ICMPEQ = 159; // 0x9F + public static final int IF_ICMPNE = 160; // 0xA0 + public static final int IF_ICMPLT = 161; // 0xA1 + public static final int IF_ICMPGE = 162; // 0xA2 + public static final int IF_ICMPGT = 163; // 0xA3 + public static final int IF_ICMPLE = 164; // 0xA4 + public static final int IF_ACMPEQ = 165; // 0xA5 + public static final int IF_ACMPNE = 166; // 0xA6 + public static final int GOTO = 167; // 0xA7 + public static final int JSR = 168; // 0xA8 + public static final int RET = 169; // 0xA9 + public static final int TABLESWITCH = 170; // 0xAA + public static final int LOOKUPSWITCH = 171; // 0xAB + public static final int IRETURN = 172; // 0xAC + public static final int LRETURN = 173; // 0xAD + public static final int FRETURN = 174; // 0xAE + public static final int DRETURN = 175; // 0xAF + public static final int ARETURN = 176; // 0xB0 + public static final int RETURN = 177; // 0xB1 + public static final int GETSTATIC = 178; // 0xB2 + public static final int PUTSTATIC = 179; // 0xB3 + public static final int GETFIELD = 180; // 0xB4 + public static final int PUTFIELD = 181; // 0xB5 + public static final int INVOKEVIRTUAL = 182; // 0xB6 + public static final int INVOKESPECIAL = 183; // 0xB7 + public static final int INVOKESTATIC = 184; // 0xB8 + public static final int INVOKEINTERFACE = 185; // 0xB9 + public static final int INVOKEDYNAMIC = 186; // 0xBA + public static final int NEW = 187; // 0xBB + public static final int NEWARRAY = 188; // 0xBC + public static final int ANEWARRAY = 189; // 0xBD + public static final int ARRAYLENGTH = 190; // 0xBE + public static final int ATHROW = 191; // 0xBF + public static final int CHECKCAST = 192; // 0xC0 + public static final int INSTANCEOF = 193; // 0xC1 + public static final int MONITORENTER = 194; // 0xC2 + public static final int MONITOREXIT = 195; // 0xC3 + public static final int WIDE = 196; // 0xC4 + public static final int MULTIANEWARRAY = 197; // 0xC5 + public static final int IFNULL = 198; // 0xC6 + public static final int IFNONNULL = 199; // 0xC7 + public static final int GOTO_W = 200; // 0xC8 + public static final int JSR_W = 201; // 0xC9 + public static final int BREAKPOINT = 202; // 0xCA + + public static final int ILLEGAL = 255; + public static final int END = 256; + // @formatter:on + + /** + * The last opcode defined by the JVM specification. To iterate over all JVM bytecodes: + * + *

+     * for (int opcode = 0; opcode <= Bytecodes.LAST_JVM_OPCODE; ++opcode) {
+     *     //
+     * }
+     * 
+ */ + public static final int LAST_JVM_OPCODE = JSR_W; + + /** + * A collection of flags describing various bytecode attributes. + */ + static class Flags { + + /** + * Denotes an instruction that ends a basic block and does not let control flow fall through + * to its lexical successor. + */ + static final int STOP = 0x00000001; + + /** + * Denotes an instruction that ends a basic block and may let control flow fall through to + * its lexical successor. In practice this means it is a conditional branch. + */ + static final int FALL_THROUGH = 0x00000002; + + /** + * Denotes an instruction that has a 2 or 4 byte operand that is an offset to another + * instruction in the same method. This does not include the {@link Bytecodes#TABLESWITCH} + * or {@link Bytecodes#LOOKUPSWITCH} instructions. + */ + static final int BRANCH = 0x00000004; + + /** + * Denotes an instruction that reads the value of a static or instance field. + */ + static final int FIELD_READ = 0x00000008; + + /** + * Denotes an instruction that writes the value of a static or instance field. + */ + static final int FIELD_WRITE = 0x00000010; + + /** + * Denotes an instruction that can cause a trap. + */ + static final int TRAP = 0x00000080; + /** + * Denotes an instruction that is commutative. + */ + static final int COMMUTATIVE = 0x00000100; + /** + * Denotes an instruction that is associative. + */ + static final int ASSOCIATIVE = 0x00000200; + /** + * Denotes an instruction that loads an operand. + */ + static final int LOAD = 0x00000400; + /** + * Denotes an instruction that stores an operand. + */ + static final int STORE = 0x00000800; + /** + * Denotes the 4 INVOKE* instructions. + */ + static final int INVOKE = 0x00001000; + } + + // Performs a sanity check that none of the flags overlap. + static { + int allFlags = 0; + try { + for (Field field : Flags.class.getDeclaredFields()) { + int flagsFilter = Modifier.FINAL | Modifier.STATIC; + if ((field.getModifiers() & flagsFilter) == flagsFilter && !field.isSynthetic()) { + assert field.getType() == int.class : "Field is not int : " + field; + final int flag = field.getInt(null); + assert flag != 0; + assert (flag & allFlags) == 0 : field.getName() + " has a value conflicting with another flag"; + allFlags |= flag; + } + } + } catch (Exception e) { + throw new InternalError(e.toString()); + } + } + + /** + * An array that maps from a bytecode value to a {@link String} for the corresponding + * instruction mnemonic. + */ + private static final String[] nameArray = new String[256]; + + /** + * An array that maps from a bytecode value to the set of {@link Flags} for the corresponding + * instruction. + */ + private static final int[] flagsArray = new int[256]; + + /** + * An array that maps from a bytecode value to the length in bytes for the corresponding + * instruction. + */ + private static final int[] lengthArray = new int[256]; + + /** + * An array that maps from a bytecode value to the number of slots pushed on the stack by the + * corresponding instruction. + */ + private static final int[] stackEffectArray = new int[256]; + + // Checkstyle: stop + // @formatter:off + static { + def(NOP , "nop" , "b" , 0); + def(ACONST_NULL , "aconst_null" , "b" , 1); + def(ICONST_M1 , "iconst_m1" , "b" , 1); + def(ICONST_0 , "iconst_0" , "b" , 1); + def(ICONST_1 , "iconst_1" , "b" , 1); + def(ICONST_2 , "iconst_2" , "b" , 1); + def(ICONST_3 , "iconst_3" , "b" , 1); + def(ICONST_4 , "iconst_4" , "b" , 1); + def(ICONST_5 , "iconst_5" , "b" , 1); + def(LCONST_0 , "lconst_0" , "b" , 2); + def(LCONST_1 , "lconst_1" , "b" , 2); + def(FCONST_0 , "fconst_0" , "b" , 1); + def(FCONST_1 , "fconst_1" , "b" , 1); + def(FCONST_2 , "fconst_2" , "b" , 1); + def(DCONST_0 , "dconst_0" , "b" , 2); + def(DCONST_1 , "dconst_1" , "b" , 2); + def(BIPUSH , "bipush" , "bc" , 1); + def(SIPUSH , "sipush" , "bcc" , 1); + def(LDC , "ldc" , "bi" , 1, TRAP); + def(LDC_W , "ldc_w" , "bii" , 1, TRAP); + def(LDC2_W , "ldc2_w" , "bii" , 2, TRAP); + def(ILOAD , "iload" , "bi" , 1, LOAD); + def(LLOAD , "lload" , "bi" , 2, LOAD); + def(FLOAD , "fload" , "bi" , 1, LOAD); + def(DLOAD , "dload" , "bi" , 2, LOAD); + def(ALOAD , "aload" , "bi" , 1, LOAD); + def(ILOAD_0 , "iload_0" , "b" , 1, LOAD); + def(ILOAD_1 , "iload_1" , "b" , 1, LOAD); + def(ILOAD_2 , "iload_2" , "b" , 1, LOAD); + def(ILOAD_3 , "iload_3" , "b" , 1, LOAD); + def(LLOAD_0 , "lload_0" , "b" , 2, LOAD); + def(LLOAD_1 , "lload_1" , "b" , 2, LOAD); + def(LLOAD_2 , "lload_2" , "b" , 2, LOAD); + def(LLOAD_3 , "lload_3" , "b" , 2, LOAD); + def(FLOAD_0 , "fload_0" , "b" , 1, LOAD); + def(FLOAD_1 , "fload_1" , "b" , 1, LOAD); + def(FLOAD_2 , "fload_2" , "b" , 1, LOAD); + def(FLOAD_3 , "fload_3" , "b" , 1, LOAD); + def(DLOAD_0 , "dload_0" , "b" , 2, LOAD); + def(DLOAD_1 , "dload_1" , "b" , 2, LOAD); + def(DLOAD_2 , "dload_2" , "b" , 2, LOAD); + def(DLOAD_3 , "dload_3" , "b" , 2, LOAD); + def(ALOAD_0 , "aload_0" , "b" , 1, LOAD); + def(ALOAD_1 , "aload_1" , "b" , 1, LOAD); + def(ALOAD_2 , "aload_2" , "b" , 1, LOAD); + def(ALOAD_3 , "aload_3" , "b" , 1, LOAD); + def(IALOAD , "iaload" , "b" , -1, TRAP); + def(LALOAD , "laload" , "b" , 0, TRAP); + def(FALOAD , "faload" , "b" , -1, TRAP); + def(DALOAD , "daload" , "b" , 0, TRAP); + def(AALOAD , "aaload" , "b" , -1, TRAP); + def(BALOAD , "baload" , "b" , -1, TRAP); + def(CALOAD , "caload" , "b" , -1, TRAP); + def(SALOAD , "saload" , "b" , -1, TRAP); + def(ISTORE , "istore" , "bi" , -1, STORE); + def(LSTORE , "lstore" , "bi" , -2, STORE); + def(FSTORE , "fstore" , "bi" , -1, STORE); + def(DSTORE , "dstore" , "bi" , -2, STORE); + def(ASTORE , "astore" , "bi" , -1, STORE); + def(ISTORE_0 , "istore_0" , "b" , -1, STORE); + def(ISTORE_1 , "istore_1" , "b" , -1, STORE); + def(ISTORE_2 , "istore_2" , "b" , -1, STORE); + def(ISTORE_3 , "istore_3" , "b" , -1, STORE); + def(LSTORE_0 , "lstore_0" , "b" , -2, STORE); + def(LSTORE_1 , "lstore_1" , "b" , -2, STORE); + def(LSTORE_2 , "lstore_2" , "b" , -2, STORE); + def(LSTORE_3 , "lstore_3" , "b" , -2, STORE); + def(FSTORE_0 , "fstore_0" , "b" , -1, STORE); + def(FSTORE_1 , "fstore_1" , "b" , -1, STORE); + def(FSTORE_2 , "fstore_2" , "b" , -1, STORE); + def(FSTORE_3 , "fstore_3" , "b" , -1, STORE); + def(DSTORE_0 , "dstore_0" , "b" , -2, STORE); + def(DSTORE_1 , "dstore_1" , "b" , -2, STORE); + def(DSTORE_2 , "dstore_2" , "b" , -2, STORE); + def(DSTORE_3 , "dstore_3" , "b" , -2, STORE); + def(ASTORE_0 , "astore_0" , "b" , -1, STORE); + def(ASTORE_1 , "astore_1" , "b" , -1, STORE); + def(ASTORE_2 , "astore_2" , "b" , -1, STORE); + def(ASTORE_3 , "astore_3" , "b" , -1, STORE); + def(IASTORE , "iastore" , "b" , -3, TRAP); + def(LASTORE , "lastore" , "b" , -4, TRAP); + def(FASTORE , "fastore" , "b" , -3, TRAP); + def(DASTORE , "dastore" , "b" , -4, TRAP); + def(AASTORE , "aastore" , "b" , -3, TRAP); + def(BASTORE , "bastore" , "b" , -3, TRAP); + def(CASTORE , "castore" , "b" , -3, TRAP); + def(SASTORE , "sastore" , "b" , -3, TRAP); + def(POP , "pop" , "b" , -1); + def(POP2 , "pop2" , "b" , -2); + def(DUP , "dup" , "b" , 1); + def(DUP_X1 , "dup_x1" , "b" , 1); + def(DUP_X2 , "dup_x2" , "b" , 1); + def(DUP2 , "dup2" , "b" , 2); + def(DUP2_X1 , "dup2_x1" , "b" , 2); + def(DUP2_X2 , "dup2_x2" , "b" , 2); + def(SWAP , "swap" , "b" , 0); + def(IADD , "iadd" , "b" , -1, COMMUTATIVE | ASSOCIATIVE); + def(LADD , "ladd" , "b" , -2, COMMUTATIVE | ASSOCIATIVE); + def(FADD , "fadd" , "b" , -1, COMMUTATIVE | ASSOCIATIVE); + def(DADD , "dadd" , "b" , -2, COMMUTATIVE | ASSOCIATIVE); + def(ISUB , "isub" , "b" , -1); + def(LSUB , "lsub" , "b" , -2); + def(FSUB , "fsub" , "b" , -1); + def(DSUB , "dsub" , "b" , -2); + def(IMUL , "imul" , "b" , -1, COMMUTATIVE | ASSOCIATIVE); + def(LMUL , "lmul" , "b" , -2, COMMUTATIVE | ASSOCIATIVE); + def(FMUL , "fmul" , "b" , -1, COMMUTATIVE | ASSOCIATIVE); + def(DMUL , "dmul" , "b" , -2, COMMUTATIVE | ASSOCIATIVE); + def(IDIV , "idiv" , "b" , -1, TRAP); + def(LDIV , "ldiv" , "b" , -2, TRAP); + def(FDIV , "fdiv" , "b" , -1); + def(DDIV , "ddiv" , "b" , -2); + def(IREM , "irem" , "b" , -1, TRAP); + def(LREM , "lrem" , "b" , -2, TRAP); + def(FREM , "frem" , "b" , -1); + def(DREM , "drem" , "b" , -2); + def(INEG , "ineg" , "b" , 0); + def(LNEG , "lneg" , "b" , 0); + def(FNEG , "fneg" , "b" , 0); + def(DNEG , "dneg" , "b" , 0); + def(ISHL , "ishl" , "b" , -1); + def(LSHL , "lshl" , "b" , -1); + def(ISHR , "ishr" , "b" , -1); + def(LSHR , "lshr" , "b" , -1); + def(IUSHR , "iushr" , "b" , -1); + def(LUSHR , "lushr" , "b" , -1); + def(IAND , "iand" , "b" , -1, COMMUTATIVE | ASSOCIATIVE); + def(LAND , "land" , "b" , -2, COMMUTATIVE | ASSOCIATIVE); + def(IOR , "ior" , "b" , -1, COMMUTATIVE | ASSOCIATIVE); + def(LOR , "lor" , "b" , -2, COMMUTATIVE | ASSOCIATIVE); + def(IXOR , "ixor" , "b" , -1, COMMUTATIVE | ASSOCIATIVE); + def(LXOR , "lxor" , "b" , -2, COMMUTATIVE | ASSOCIATIVE); + def(IINC , "iinc" , "bic" , 0, LOAD | STORE); + def(I2L , "i2l" , "b" , 1); + def(I2F , "i2f" , "b" , 0); + def(I2D , "i2d" , "b" , 1); + def(L2I , "l2i" , "b" , -1); + def(L2F , "l2f" , "b" , -1); + def(L2D , "l2d" , "b" , 0); + def(F2I , "f2i" , "b" , 0); + def(F2L , "f2l" , "b" , 1); + def(F2D , "f2d" , "b" , 1); + def(D2I , "d2i" , "b" , -1); + def(D2L , "d2l" , "b" , 0); + def(D2F , "d2f" , "b" , -1); + def(I2B , "i2b" , "b" , 0); + def(I2C , "i2c" , "b" , 0); + def(I2S , "i2s" , "b" , 0); + def(LCMP , "lcmp" , "b" , -3); + def(FCMPL , "fcmpl" , "b" , -1); + def(FCMPG , "fcmpg" , "b" , -1); + def(DCMPL , "dcmpl" , "b" , -3); + def(DCMPG , "dcmpg" , "b" , -3); + def(IFEQ , "ifeq" , "boo" , -1, FALL_THROUGH | BRANCH); + def(IFNE , "ifne" , "boo" , -1, FALL_THROUGH | BRANCH); + def(IFLT , "iflt" , "boo" , -1, FALL_THROUGH | BRANCH); + def(IFGE , "ifge" , "boo" , -1, FALL_THROUGH | BRANCH); + def(IFGT , "ifgt" , "boo" , -1, FALL_THROUGH | BRANCH); + def(IFLE , "ifle" , "boo" , -1, FALL_THROUGH | BRANCH); + def(IF_ICMPEQ , "if_icmpeq" , "boo" , -2, COMMUTATIVE | FALL_THROUGH | BRANCH); + def(IF_ICMPNE , "if_icmpne" , "boo" , -2, COMMUTATIVE | FALL_THROUGH | BRANCH); + def(IF_ICMPLT , "if_icmplt" , "boo" , -2, FALL_THROUGH | BRANCH); + def(IF_ICMPGE , "if_icmpge" , "boo" , -2, FALL_THROUGH | BRANCH); + def(IF_ICMPGT , "if_icmpgt" , "boo" , -2, FALL_THROUGH | BRANCH); + def(IF_ICMPLE , "if_icmple" , "boo" , -2, FALL_THROUGH | BRANCH); + def(IF_ACMPEQ , "if_acmpeq" , "boo" , -2, COMMUTATIVE | FALL_THROUGH | BRANCH); + def(IF_ACMPNE , "if_acmpne" , "boo" , -2, COMMUTATIVE | FALL_THROUGH | BRANCH); + def(GOTO , "goto" , "boo" , 0, STOP | BRANCH); + def(JSR , "jsr" , "boo" , 0, STOP | BRANCH); + def(RET , "ret" , "bi" , 0, STOP); + def(TABLESWITCH , "tableswitch" , "" , -1, STOP); + def(LOOKUPSWITCH , "lookupswitch" , "" , -1, STOP); + def(IRETURN , "ireturn" , "b" , -1, TRAP | STOP); + def(LRETURN , "lreturn" , "b" , -2, TRAP | STOP); + def(FRETURN , "freturn" , "b" , -1, TRAP | STOP); + def(DRETURN , "dreturn" , "b" , -2, TRAP | STOP); + def(ARETURN , "areturn" , "b" , -1, TRAP | STOP); + def(RETURN , "return" , "b" , 0, TRAP | STOP); + def(GETSTATIC , "getstatic" , "bjj" , 1, TRAP | FIELD_READ); + def(PUTSTATIC , "putstatic" , "bjj" , -1, TRAP | FIELD_WRITE); + def(GETFIELD , "getfield" , "bjj" , 0, TRAP | FIELD_READ); + def(PUTFIELD , "putfield" , "bjj" , -2, TRAP | FIELD_WRITE); + def(INVOKEVIRTUAL , "invokevirtual" , "bjj" , -1, TRAP | INVOKE); + def(INVOKESPECIAL , "invokespecial" , "bjj" , -1, TRAP | INVOKE); + def(INVOKESTATIC , "invokestatic" , "bjj" , 0, TRAP | INVOKE); + def(INVOKEINTERFACE , "invokeinterface" , "bjja_", -1, TRAP | INVOKE); + def(INVOKEDYNAMIC , "invokedynamic" , "bjjjj", 0, TRAP | INVOKE); + def(NEW , "new" , "bii" , 1, TRAP); + def(NEWARRAY , "newarray" , "bc" , 0, TRAP); + def(ANEWARRAY , "anewarray" , "bii" , 0, TRAP); + def(ARRAYLENGTH , "arraylength" , "b" , 0, TRAP); + def(ATHROW , "athrow" , "b" , -1, TRAP | STOP); + def(CHECKCAST , "checkcast" , "bii" , 0, TRAP); + def(INSTANCEOF , "instanceof" , "bii" , 0, TRAP); + def(MONITORENTER , "monitorenter" , "b" , -1, TRAP); + def(MONITOREXIT , "monitorexit" , "b" , -1, TRAP); + def(WIDE , "wide" , "" , 0); + def(MULTIANEWARRAY , "multianewarray" , "biic" , 1, TRAP); + def(IFNULL , "ifnull" , "boo" , -1, FALL_THROUGH | BRANCH); + def(IFNONNULL , "ifnonnull" , "boo" , -1, FALL_THROUGH | BRANCH); + def(GOTO_W , "goto_w" , "boooo", 0, STOP | BRANCH); + def(JSR_W , "jsr_w" , "boooo", 0, STOP | BRANCH); + def(BREAKPOINT , "breakpoint" , "b" , 0, TRAP); + } + // @formatter:on + // Checkstyle: resume + + /** + * Gets the length of an instruction denoted by a given opcode. + * + * @param opcode an instruction opcode + * @return the length of the instruction denoted by {@code opcode}. If {@code opcode} is an + * illegal instruction or denotes a variable length instruction (e.g. + * {@link #TABLESWITCH}), then 0 is returned. + */ + public static int lengthOf(int opcode) { + return lengthArray[opcode & 0xff]; + } + + /** + * Gets the effect on the depth of the expression stack of an instruction denoted by a given + * opcode. + * + * @param opcode an instruction opcode + * @return the change in the stack caused by the instruction denoted by {@code opcode}. If + * {@code opcode} is an illegal instruction then 0 is returned. Note that invoke + * instructions may pop more arguments so this value is a minimum stack effect. + */ + public static int stackEffectOf(int opcode) { + return stackEffectArray[opcode & 0xff]; + } + + /** + * Gets the lower-case mnemonic for a given opcode. + * + * @param opcode an opcode + * @return the mnemonic for {@code opcode} or {@code ""} if + * {@code opcode} is not a legal opcode + */ + public static String nameOf(int opcode) throws IllegalArgumentException { + String name = nameArray[opcode & 0xff]; + if (name == null) { + return ""; + } + return name; + } + + /** + * Gets the opcode corresponding to a given mnemonic. + * + * @param name an opcode mnemonic + * @return the opcode corresponding to {@code mnemonic} + * @throws IllegalArgumentException if {@code name} does not denote a valid opcode + */ + public static int valueOf(String name) { + for (int opcode = 0; opcode < nameArray.length; ++opcode) { + if (name.equalsIgnoreCase(nameArray[opcode])) { + return opcode; + } + } + throw new IllegalArgumentException("No opcode for " + name); + } + + /** + * Determines if a given opcode is an instruction that has a 2 or 4 byte operand that is an + * offset to another instruction in the same method. This does not include the + * {@linkplain #TABLESWITCH switch} instructions. + * + * @param opcode an opcode to test + * @return {@code true} iff {@code opcode} is a branch instruction with a single operand + */ + public static boolean isBranch(int opcode) { + return (flagsArray[opcode & 0xff] & BRANCH) != 0; + } + + /** + * Determines if a given opcode denotes a conditional branch. + * + * @param opcode + * @return {@code true} iff {@code opcode} is a conditional branch + */ + public static boolean isConditionalBranch(int opcode) { + return (flagsArray[opcode & 0xff] & FALL_THROUGH) != 0; + } + + /** + * Determines if a given opcode denotes an invoke. + * + * @param opcode + * @return {@code true} iff {@code opcode} is an invoke + */ + public static boolean isInvoke(int opcode) { + return (flagsArray[opcode & 0xff] & INVOKE) != 0; + } + + /** + * Gets the arithmetic operator name for a given opcode. If {@code opcode} does not denote an + * arithmetic instruction, then the {@linkplain #nameOf(int) name} of the opcode is returned + * instead. + * + * @param op an opcode + * @return the arithmetic operator name + */ + public static String operator(int op) { + // Checkstyle: stop + switch (op) { + // arithmetic ops + case IADD: // fall through + case LADD: // fall through + case FADD: // fall through + case DADD: + return "+"; + case ISUB: // fall through + case LSUB: // fall through + case FSUB: // fall through + case DSUB: + return "-"; + case IMUL: // fall through + case LMUL: // fall through + case FMUL: // fall through + case DMUL: + return "*"; + case IDIV: // fall through + case LDIV: // fall through + case FDIV: // fall through + case DDIV: + return "/"; + case IREM: // fall through + case LREM: // fall through + case FREM: // fall through + case DREM: + return "%"; + // shift ops + case ISHL: // fall through + case LSHL: + return "<<"; + case ISHR: // fall through + case LSHR: + return ">>"; + case IUSHR: // fall through + case LUSHR: + return ">>>"; + // logic ops + case IAND: // fall through + case LAND: + return "&"; + case IOR: // fall through + case LOR: + return "|"; + case IXOR: // fall through + case LXOR: + return "^"; + } + // Checkstyle: resume + return nameOf(op); + } + + /** + * Defines a bytecode by entering it into the arrays that record its name, length and flags. + * + * @param name instruction name (should be lower case) + * @param format encodes the length of the instruction + */ + private static void def(int opcode, String name, String format, int stackEffect) { + def(opcode, name, format, stackEffect, 0); + } + + /** + * Defines a bytecode by entering it into the arrays that record its name, length and flags. + * + * @param name instruction name (lower case) + * @param format encodes the length of the instruction + * @param flags the set of {@link Flags} associated with the instruction + */ + private static void def(int opcode, String name, String format, int stackEffect, int flags) { + assert nameArray[opcode] == null : "opcode " + opcode + " is already bound to name " + nameArray[opcode]; + nameArray[opcode] = name; + int instructionLength = format.length(); + lengthArray[opcode] = instructionLength; + stackEffectArray[opcode] = stackEffect; + Bytecodes.flagsArray[opcode] = flags; + + assert !isConditionalBranch(opcode) || isBranch(opcode) : "a conditional branch must also be a branch"; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterConstantPool.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterConstantPool.java new file mode 100644 index 000000000000..3b3fd16a8268 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterConstantPool.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEDYNAMIC; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.heap.UnknownObjectField; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.serialization.VisibleForSerialization; + +import jdk.vm.ci.meta.ConstantPool; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaField; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.Signature; + +public final class InterpreterConstantPool implements ConstantPool { + + private final InterpreterResolvedObjectType holder; + // Assigned after analysis. + @UnknownObjectField(types = Object[].class) private Object[] entries; + + Object at(int cpi) { + if (cpi == 0) { + // 0 implies unknown (!= unresolved) e.g. unknown class, field, method ... + // In this case it's not possible to even provide a name or symbolic representation for + // what's missing. + // Index 0 must be handled by the resolution methods e.g. resolveType, resolveMethod ... + // where an appropriate error should be thrown. + throw VMError.shouldNotReachHere("Cannot resolve CP entry 0"); + } + return entries[cpi]; + } + + private InterpreterConstantPool(InterpreterResolvedObjectType holder, Object[] entries) { + this.holder = MetadataUtil.requireNonNull(holder); + this.entries = MetadataUtil.requireNonNull(entries); + } + + @VisibleForSerialization + public static InterpreterConstantPool create(InterpreterResolvedObjectType holder, Object[] entries) { + return new InterpreterConstantPool(holder, entries); + } + + @Override + public int length() { + return entries.length; + } + + @Override + public JavaField lookupField(int cpi, ResolvedJavaMethod method, int opcode) { + return (JavaField) at(cpi); + } + + @Override + public JavaMethod lookupMethod(int cpi, int opcode) { + return (JavaMethod) at(cpi); + } + + @Override + public JavaMethod lookupMethod(int cpi, int opcode, ResolvedJavaMethod caller) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public JavaType lookupType(int cpi, int opcode) { + return (JavaType) at(cpi); + } + + @Override + public Object lookupConstant(int cpi) { + Object entry = at(cpi); + if (entry instanceof JavaConstant) { + return entry; + } else if (entry instanceof JavaType) { + return entry; + } + throw VMError.shouldNotReachHereAtRuntime(); + } + + @Override + public Object lookupConstant(int cpi, boolean resolve) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public JavaConstant lookupAppendix(int cpi, int opcode) { + assert opcode == INVOKEDYNAMIC; + return (JavaConstant) at(cpi); + } + + @VisibleForSerialization + @Platforms(Platform.HOSTED_ONLY.class) + public Object[] getEntries() { + return entries; + } + + public InterpreterResolvedObjectType getHolder() { + return holder; + } + + // region Unimplemented methods + + @Override + public void loadReferencedType(int cpi, int opcode) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public JavaType lookupReferencedType(int cpi, int opcode) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public String lookupUtf8(int cpi) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public Signature lookupSignature(int cpi) { + throw VMError.intentionallyUnimplemented(); + } + + // endregion Unimplemented methods +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaField.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaField.java new file mode 100644 index 000000000000..9a8ac79393cd --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaField.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import java.lang.annotation.Annotation; + +import com.oracle.svm.core.hub.DynamicHub; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.PrimitiveConstant; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.VMError; + +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.ResolvedJavaField; + +public final class InterpreterResolvedJavaField implements ResolvedJavaField { + + // Computed after analysis. + private int offset; + private final int modifiers; + private final String name; + + private final InterpreterResolvedJavaType type; + + private final InterpreterResolvedObjectType declaringClass; + + private JavaConstant constantValue; + + @Platforms(Platform.HOSTED_ONLY.class) private ResolvedJavaField originalField; + + /** + * Ensures that the field metadata is kept for the interpreter, without forcing it into the + * image. Reachability still depends on the declaring class and field type reachability. + * Artificial reachability is useful for fields that are reachable at build-time e.g. inlined or + * substituted. They are never accessed again at runtime in compiled code, but the interpreter + * may still access them e.g. $assertionsDisabled. + */ + @Platforms(Platform.HOSTED_ONLY.class) private boolean artificiallyReachable; + + @Platforms(Platform.HOSTED_ONLY.class) + private InterpreterResolvedJavaField(ResolvedJavaField originalField, String name, int modifiers, InterpreterResolvedJavaType type, InterpreterResolvedObjectType declaringClass, int offset, + JavaConstant constant) { + this.originalField = originalField; + this.name = MetadataUtil.requireNonNull(name); + this.modifiers = modifiers; + this.type = MetadataUtil.requireNonNull(type); + this.declaringClass = MetadataUtil.requireNonNull(declaringClass); + this.offset = offset; + this.constantValue = constant; + } + + private InterpreterResolvedJavaField(String name, int modifiers, InterpreterResolvedJavaType type, InterpreterResolvedObjectType declaringClass, int offset, JavaConstant constant) { + this.name = MetadataUtil.requireNonNull(name); + this.modifiers = modifiers; + this.type = MetadataUtil.requireNonNull(type); + this.declaringClass = MetadataUtil.requireNonNull(declaringClass); + this.offset = offset; + this.constantValue = constant; + } + + public static InterpreterResolvedJavaField create(String name, int modifiers, InterpreterResolvedJavaType type, InterpreterResolvedObjectType declaringClass, int offset, JavaConstant constant) { + return new InterpreterResolvedJavaField(name, modifiers, type, declaringClass, offset, constant); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static InterpreterResolvedJavaField create(ResolvedJavaField originalField, String name, int modifiers, InterpreterResolvedJavaType type, InterpreterResolvedObjectType declaringClass, + int offset, JavaConstant constant) { + return new InterpreterResolvedJavaField(originalField, name, modifiers, type, declaringClass, offset, constant); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public ResolvedJavaField getOriginalField() { + return originalField; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setUnmaterializedConstant(JavaConstant constant) { + assert JavaConstant.NULL_POINTER.equals(constant) || constant instanceof PrimitiveConstant || constant instanceof ReferenceConstant; + this.offset = InterpreterResolvedJavaField.FIELD_UNMATERIALIZED; + this.constantValue = constant; + } + + public static final int FIELD_UNMATERIALIZED = -10; + + public boolean isUnmaterializedConstant() { + return this.offset == FIELD_UNMATERIALIZED; + } + + /** + * A field is undefined when it is unmaterialized, and the value is not preserved for the + * interpreter. Examples of undefined fields include: {@link jdk.graal.compiler.word.Word} + * subtypes, {@link DynamicHub}'s vtable. + */ + public boolean isUndefined() { + return this.isUnmaterializedConstant() && + this.getUnmaterializedConstant().getJavaKind() == JavaKind.Illegal; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + @Override + public int getModifiers() { + return modifiers; + } + + @Override + public int getOffset() { + return offset; + } + + @Override + public String getName() { + return name; + } + + @Override + public InterpreterResolvedJavaType getType() { + return type; + } + + @Override + public InterpreterResolvedObjectType getDeclaringClass() { + return declaringClass; + } + + public JavaConstant getUnmaterializedConstant() { + assert offset == FIELD_UNMATERIALIZED; + // constantValue can be "Illegal" for some folded constants, for which the value is not + // stored in the image heap. + // Also take into account WordBase types, which have an Object kind, but the constantValue + // is a long. + assert this.getType().isWordType() || + constantValue == JavaConstant.ILLEGAL || getJavaKind() == constantValue.getJavaKind(); + return constantValue; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public boolean isArtificiallyReachable() { + return artificiallyReachable; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void markAsArtificiallyReachable() { + this.artificiallyReachable = true; + } + + @Override + public String toString() { + return "InterpreterResolvedJavaField"; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof InterpreterResolvedJavaField)) { + return false; + } + InterpreterResolvedJavaField that = (InterpreterResolvedJavaField) other; + return name.equals(that.name) && declaringClass.equals(that.declaringClass) && type.equals(that.type); + } + + @Override + public int hashCode() { + int result = MetadataUtil.hashCode(name); + result = 31 * result + MetadataUtil.hashCode(declaringClass); + result = 31 * result + MetadataUtil.hashCode(type); + return result; + } + + // region Unimplemented methods + + @Override + public boolean isInternal() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean isSynthetic() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public T getAnnotation(Class annotationClass) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public Annotation[] getAnnotations() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + throw VMError.intentionallyUnimplemented(); + } + + // endregion Unimplemented methods +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaMethod.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaMethod.java new file mode 100644 index 000000000000..8c85a557ca74 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaMethod.java @@ -0,0 +1,581 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.BREAKPOINT; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.FunctionPointerHolder; +import com.oracle.svm.core.meta.MethodPointer; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.serialization.VisibleForSerialization; + +import jdk.vm.ci.meta.Constant; +import jdk.vm.ci.meta.ExceptionHandler; +import jdk.vm.ci.meta.LineNumberTable; +import jdk.vm.ci.meta.Local; +import jdk.vm.ci.meta.LocalVariableTable; +import jdk.vm.ci.meta.ProfilingInfo; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.SpeculationLog; + +/** + * Encapsulates resolved methods used under close-world assumptions, compiled and interpretable, but + * also abstract methods for vtable calls. + */ +public final class InterpreterResolvedJavaMethod implements ResolvedJavaMethod { + + public static final LocalVariableTable EMPTY_LOCAL_VARIABLE_TABLE = new LocalVariableTable(new Local[0]); + + public static final int UNKNOWN_METHOD_ID = 0; + + // Should be final (not its contents, it can be patched with BREAKPOINT). + // These are the bytecodes executed by the interpreter e.g. can be patched with BREAKPOINT. + private byte[] interpretedCode; + private final String name; + private final int maxLocals; + private final int maxStackSize; + private final int modifiers; + + @Platforms(Platform.HOSTED_ONLY.class) // + private ResolvedJavaMethod originalMethod; + + private final InterpreterResolvedObjectType declaringClass; + private final InterpreterUnresolvedSignature signature; + + private final LineNumberTable lineNumberTable; + + private ExceptionHandler[] exceptionHandlers; + + private LocalVariableTable localVariableTable; + + private ReferenceConstant nativeEntryPoint; + + // Token set by the toggle of method enter/exit events. + private volatile Object interpreterExecToken; + + public static class InlinedBy { + public InterpreterResolvedJavaMethod holder; + public Set inliners; + + public InlinedBy(InterpreterResolvedJavaMethod holder, Set inliners) { + this.holder = holder; + this.inliners = inliners; + } + } + + protected InlinedBy inlinedBy; + + public static final int VTBL_NO_ENTRY = -1; + public static final int VTBL_ONE_IMPL = -2; + private int vtableIndex = VTBL_NO_ENTRY; + private InterpreterResolvedJavaMethod oneImplementation; + + /* slot in GOT */ + private int gotOffset; + + public static final int EST_NO_ENTRY = -5; + /* slot in EST (EnterStubTable) */ + private int enterStubOffset = EST_NO_ENTRY; + + /** + * Unique identifier for methods in compiled frames. + *

+ * Only valid if != 0, 0 means unknown. Allows to precisely extract the + * {@link InterpreterResolvedJavaMethod interpreter method instance} from a compiled frame. + */ + private int methodId; + + @Platforms(Platform.HOSTED_ONLY.class) public boolean needMethodBody; + + // Only called during universe building + @Platforms(Platform.HOSTED_ONLY.class) + private InterpreterResolvedJavaMethod(ResolvedJavaMethod originalMethod, String name, int maxLocals, int maxStackSize, int modifiers, InterpreterResolvedObjectType declaringClass, + InterpreterUnresolvedSignature signature, + byte[] code, ExceptionHandler[] exceptionHandlers, LineNumberTable lineNumberTable, LocalVariableTable localVariableTable, + ReferenceConstant nativeEntryPoint, int vtableIndex, int gotOffset, int enterStubOffset, int methodId) { + this(name, maxLocals, maxStackSize, modifiers, declaringClass, signature, code, exceptionHandlers, lineNumberTable, localVariableTable, nativeEntryPoint, vtableIndex, gotOffset, + enterStubOffset, methodId); + this.originalMethod = originalMethod; + this.needMethodBody = false; + this.inlinedBy = new InterpreterResolvedJavaMethod.InlinedBy(this, new HashSet<>()); + } + + private InterpreterResolvedJavaMethod(String name, int maxLocals, int maxStackSize, int modifiers, InterpreterResolvedObjectType declaringClass, InterpreterUnresolvedSignature signature, + byte[] code, ExceptionHandler[] exceptionHandlers, LineNumberTable lineNumberTable, LocalVariableTable localVariableTable, + ReferenceConstant nativeEntryPoint, int vtableIndex, int gotOffset, int enterStubOffset, int methodId) { + this.name = name; + this.maxLocals = maxLocals; + this.maxStackSize = maxStackSize; + this.modifiers = modifiers; + this.declaringClass = declaringClass; + this.signature = signature; + this.interpretedCode = code; + this.exceptionHandlers = exceptionHandlers; + this.lineNumberTable = lineNumberTable; + this.localVariableTable = localVariableTable; + + this.nativeEntryPoint = nativeEntryPoint; + this.vtableIndex = vtableIndex; + this.gotOffset = gotOffset; + this.enterStubOffset = enterStubOffset; + this.methodId = methodId; + this.inlinedBy = new InlinedBy(this, new HashSet<>()); + } + + @VisibleForSerialization + public static InterpreterResolvedJavaMethod create(String name, int maxLocals, int maxStackSize, int modifiers, InterpreterResolvedObjectType declaringClass, + InterpreterUnresolvedSignature signature, + byte[] code, ExceptionHandler[] exceptionHandlers, LineNumberTable lineNumberTable, LocalVariableTable localVariableTable, + ReferenceConstant nativeEntryPoint, int vtableIndex, int gotOffset, int enterStubOffset, int methodId) { + return new InterpreterResolvedJavaMethod(name, maxLocals, maxStackSize, modifiers, declaringClass, signature, code, + exceptionHandlers, lineNumberTable, localVariableTable, nativeEntryPoint, vtableIndex, gotOffset, enterStubOffset, methodId); + } + + // Only called during universe building + @Platforms(Platform.HOSTED_ONLY.class) + public static InterpreterResolvedJavaMethod create(ResolvedJavaMethod originalMethod, String name, int maxLocals, int maxStackSize, int modifiers, InterpreterResolvedObjectType declaringClass, + InterpreterUnresolvedSignature signature, + byte[] code, ExceptionHandler[] exceptionHandlers, LineNumberTable lineNumberTable, LocalVariableTable localVariableTable, + ReferenceConstant nativeEntryPoint, int vtableIndex, int gotOffset, int enterStubOffset, int methodId) { + return new InterpreterResolvedJavaMethod(originalMethod, name, maxLocals, maxStackSize, modifiers, declaringClass, signature, code, + exceptionHandlers, lineNumberTable, localVariableTable, nativeEntryPoint, vtableIndex, gotOffset, enterStubOffset, methodId); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public boolean needsMethodBody() { + return needMethodBody; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public ResolvedJavaMethod getOriginalMethod() { + return originalMethod; + } + + /** + * Returns the bytecodes executed by the interpreter, may include BREAKPOINT and other + * non-standard bytecodes used by the interpreter. For a spec-compliant, without BREAKPOINT and + * non-standard bytecodes use {@link #getCode()} + */ + public byte[] getInterpretedCode() { + return interpretedCode; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setCode(byte[] code) { + VMError.guarantee(originalCode == null); + this.interpretedCode = code; + } + + private volatile byte[] originalCode; + + public int getOriginalOpcodeAt(int bci) { + return getCode()[bci] & 0xFF; + } + + @Override + public byte[] getCode() { + if (interpretedCode == null) { + return null; + } + byte[] result = originalCode; + if (result == null) { + synchronized (this) { + result = originalCode; + if (result == null) { + originalCode = result = getInterpretedCode().clone(); + verifySanitizedCode(result); // assert + } + } + } + return result; + } + + private static void verifySanitizedCode(byte[] code) { + for (int bci = 0; bci < BytecodeStream.endBCI(code); bci = BytecodeStream.nextBCI(code, bci)) { + int currentBC = BytecodeStream.currentBC(code, bci); + VMError.guarantee(Bytecodes.BREAKPOINT != currentBC); + } + } + + @Override + public int getCodeSize() { + if (interpretedCode == null) { + return 0; + } + return interpretedCode.length; + } + + @Override + public String getName() { + return name; + } + + @Override + public InterpreterResolvedObjectType getDeclaringClass() { + return declaringClass; + } + + @Override + public InterpreterUnresolvedSignature getSignature() { + return signature; + } + + @Override + public int getMaxLocals() { + return maxLocals; + } + + @Override + public int getMaxStackSize() { + return maxStackSize; + } + + @Override + public boolean isClassInitializer() { + return "".equals(getName()) && isStatic(); + } + + @Override + public boolean isConstructor() { + return "".equals(getName()) && !isStatic(); + } + + @Override + public ExceptionHandler[] getExceptionHandlers() { + ExceptionHandler[] result = exceptionHandlers; + VMError.guarantee(result != null); + return result; + } + + @Override + public InterpreterConstantPool getConstantPool() { + return declaringClass.getConstantPool(); + } + + @Override + public LineNumberTable getLineNumberTable() { + return lineNumberTable; + } + + @Override + public LocalVariableTable getLocalVariableTable() { + return localVariableTable; + } + + @Override + public int getModifiers() { + return modifiers; + } + + @Override + public String toString() { + return "InterpreterResolvedJavaMethod"; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setExceptionHandlers(ExceptionHandler[] exceptionHandlers) { + this.exceptionHandlers = MetadataUtil.requireNonNull(exceptionHandlers); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setLocalVariableTable(LocalVariableTable localVariableTable) { + this.localVariableTable = MetadataUtil.requireNonNull(localVariableTable); + } + + private void patchOpcode(int bci, int newOpcode) { + BytecodeStream.patchOpcodeOpaque(interpretedCode, bci, newOpcode); + } + + public void ensureCanSetBreakpointAt(int bci) { + if (!hasBytecodes()) { + throw new IllegalArgumentException("Cannot set breakpoint: method " + name + " doesn't have bytecodes"); + } + if (bci < 0 || getCodeSize() <= bci) { + throw new IllegalArgumentException("Cannot set breakpoint: BCI out of bounds"); + } + if (!isValidBCI(getCode(), bci)) { + throw new IllegalArgumentException("Cannot set breakpoint: targetBCI is not a valid first opcode"); + } + } + + public void toggleBreakpoint(int bci, boolean enabled) { + ensureCanSetBreakpointAt(bci); + if (enabled) { + patchOpcode(bci, BREAKPOINT); + } else { + int originalOpcode = getOriginalOpcodeAt(bci); + patchOpcode(bci, originalOpcode); + } + } + + public static boolean isValidBCI(byte[] code, int targetBCI) { + for (int bci = 0; bci < BytecodeStream.endBCI(code); bci = BytecodeStream.nextBCI(code, bci)) { + if (bci == targetBCI) { + return true; + } + } + return false; + } + + /** + * Unique identifier for methods in compiled frames. + *

+ * Only valid if != 0, 0 means unknown. Allows to precisely extract the + * {@link InterpreterResolvedJavaMethod interpreter method instance} from a compiled frame. + */ + public int getMethodId() { + return methodId; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setMethodId(int methodId) { + assert methodId >= 0; + this.methodId = methodId; + } + + public void setGOTOffset(int gotOffset) { + this.gotOffset = gotOffset; + } + + public int getGotOffset() { + return gotOffset; + } + + public void setEnterStubOffset(int offset) { + this.enterStubOffset = offset; + } + + public int getEnterStubOffset() { + return enterStubOffset; + } + + public boolean hasNativeEntryPoint() { + return nativeEntryPoint != null; + } + + public MethodPointer getNativeEntryPoint() { + if (nativeEntryPoint == null) { + return WordFactory.nullPointer(); + } + return (MethodPointer) nativeEntryPoint.getReferent().functionPointer; + } + + public ReferenceConstant getNativeEntryPointHolderConstant() { + return nativeEntryPoint; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setNativeEntryPoint(MethodPointer nativeEntryPoint) { + if (this.nativeEntryPoint != null && nativeEntryPoint != null) { + /* already set, verify if it's the same */ + ResolvedJavaMethod setMethod = ((MethodPointer) this.nativeEntryPoint.getReferent().functionPointer).getMethod(); + VMError.guarantee(setMethod.equals(nativeEntryPoint.getMethod())); + return; + } + + if (nativeEntryPoint == null) { + this.nativeEntryPoint = null; + } else { + this.nativeEntryPoint = ReferenceConstant.createFromNonNullReference(new FunctionPointerHolder(nativeEntryPoint)); + } + } + + public int getVTableIndex() { + return vtableIndex; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void setVTableIndex(int vtableIndex) { + VMError.guarantee(vtableIndex == VTBL_NO_ENTRY || (!isStatic() && !isConstructor())); + if (vtableIndex >= 0) { + VMError.guarantee(!isFinal()); + } + this.vtableIndex = vtableIndex; + } + + public boolean hasVTableIndex() { + return vtableIndex != VTBL_NO_ENTRY && vtableIndex != VTBL_ONE_IMPL; + } + + public void setOneImplementation(InterpreterResolvedJavaMethod oneImplementation) { + this.oneImplementation = oneImplementation; + } + + public InterpreterResolvedJavaMethod getOneImplementation() { + /* if VTBL_ONE_IMPL is set, oneImplementation must have an assignment */ + VMError.guarantee(vtableIndex != VTBL_ONE_IMPL || oneImplementation != null); + return oneImplementation; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof InterpreterResolvedJavaMethod)) { + return false; + } + InterpreterResolvedJavaMethod that = (InterpreterResolvedJavaMethod) other; + return name.equals(that.name) && declaringClass.equals(that.declaringClass) && signature.equals(that.signature); + } + + @Override + public int hashCode() { + int result = MetadataUtil.hashCode(name); + result = 31 * result + MetadataUtil.hashCode(declaringClass); + result = 31 * result + MetadataUtil.hashCode(signature); + return result; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public boolean isInterpreterExecutable() { + return hasBytecodes(); + } + + public Set getInlinedBy() { + return inlinedBy.inliners; + } + + public void addInliner(InterpreterResolvedJavaMethod inliner) { + inlinedBy.inliners.add(inliner); + } + + public Object getInterpreterExecToken() { + return interpreterExecToken; + } + + public void setInterpreterExecToken(Object interpreterExecToken) { + this.interpreterExecToken = interpreterExecToken; + } + + // region Unimplemented methods + + @Override + public Annotation[][] getParameterAnnotations() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public Type[] getGenericParameterTypes() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean canBeInlined() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean hasNeverInlineDirective() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean shouldBeInlined() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public Constant getEncoding() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean isInVirtualMethodTable(ResolvedJavaType resolved) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public SpeculationLog getSpeculationLog() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public T getAnnotation(Class annotationClass) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public Annotation[] getAnnotations() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean isSynthetic() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean isVarArgs() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean isBridge() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean isDefault() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean canBeStaticallyBound() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public StackTraceElement asStackTraceElement(int bci) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public ProfilingInfo getProfilingInfo(boolean includeNormal, boolean includeOSR) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public void reprofile() { + throw VMError.intentionallyUnimplemented(); + } + + // endregion Unimplemented methods +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaType.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaType.java new file mode 100644 index 000000000000..4ba41bb8163a --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedJavaType.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.WordBase; + +import com.oracle.svm.core.util.VMError; + +import jdk.vm.ci.meta.Assumptions; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * Represents a primitive or reference resolved Java type, including additional capabilities of the + * closed world e.g. instantiable, instantiated, effectively final ... + */ +public abstract class InterpreterResolvedJavaType implements ResolvedJavaType { + + private final String name; + private final Class clazz; + private final JavaConstant clazzConstant; + private final boolean isWordType; + private volatile boolean methodEnterEventEnabled; + private volatile boolean methodExitEventEnabled; + + // Only called at build time universe creation. + @Platforms(Platform.HOSTED_ONLY.class) + protected InterpreterResolvedJavaType(String name, Class javaClass) { + this.name = MetadataUtil.requireNonNull(name); + this.clazzConstant = null; + this.clazz = MetadataUtil.requireNonNull(javaClass); + this.isWordType = WordBase.class.isAssignableFrom(javaClass); + } + + // Called by the interpreter. + protected InterpreterResolvedJavaType(String name, Class javaClass, boolean isWordType) { + this.name = MetadataUtil.requireNonNull(name); + this.clazzConstant = null; + this.clazz = MetadataUtil.requireNonNull(javaClass); + this.isWordType = isWordType; + } + + protected InterpreterResolvedJavaType(String name, JavaConstant clazzConstant, boolean isWordType) { + this.name = MetadataUtil.requireNonNull(name); + this.clazzConstant = MetadataUtil.requireNonNull(clazzConstant); + this.clazz = null; + this.isWordType = isWordType; + } + + @Override + public final String getName() { + return name; + } + + // This is only here for performance, otherwise the clazzConstant must be unwrapped every time. + public final Class getJavaClass() { + return MetadataUtil.requireNonNull(clazz); + } + + public final boolean isWordType() { + return isWordType; + } + + @Override + public final boolean isPrimitive() { + return this instanceof InterpreterResolvedPrimitiveType; + } + + @Override + public final boolean isInterface() { + return Modifier.isInterface(getModifiers()); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + if (obj instanceof InterpreterResolvedJavaType that) { + if (this.clazz != null) { + return this.clazz == that.clazz; + } + // We have no access to the classes (debugger side), only opaque constants. + assert this.clazzConstant != null && that.clazzConstant != null; + return this.clazzConstant.equals(that.clazzConstant); + } else { + return false; + } + } + + @Override + public final int hashCode() { + return getName().hashCode(); + } + + @Override + public final String toString() { + return "InterpreterResolvedJavaType<" + getName() + ">"; + } + + public void toggleMethodEnterEvent(boolean enable) { + methodEnterEventEnabled = enable; + } + + public void toggleMethodExitEvent(boolean enable) { + methodExitEventEnabled = enable; + } + + public boolean isMethodEnterEvent() { + return methodEnterEventEnabled; + } + + public boolean isMethodExitEvent() { + return methodExitEventEnabled; + } + + // region Unimplemented methods + + @Override + public final boolean hasFinalizer() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final Assumptions.AssumptionResult hasFinalizableSubclass() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final boolean isInstanceClass() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final boolean isEnum() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final boolean isInitialized() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final void initialize() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final boolean isLinked() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final boolean isInstance(JavaConstant obj) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaType getSingleImplementor() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaType findLeastCommonAncestor(ResolvedJavaType otherType) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final Assumptions.AssumptionResult findLeafConcreteSubtype() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaType resolve(ResolvedJavaType accessingClass) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaMethod resolveMethod(ResolvedJavaMethod method, ResolvedJavaType callerType) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final Assumptions.AssumptionResult findUniqueConcreteMethod(ResolvedJavaMethod method) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaField[] getInstanceFields(boolean includeSuperclasses) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaField[] getStaticFields() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaField findInstanceFieldWithOffset(long offset, JavaKind expectedKind) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final InterpreterResolvedObjectType getArrayClass() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final boolean isLocal() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final boolean isMember() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaType getEnclosingType() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaMethod[] getDeclaredConstructors() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaMethod[] getDeclaredMethods() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final ResolvedJavaMethod getClassInitializer() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final boolean isCloneableWithAllocation() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final T getAnnotation(Class annotationClass) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final Annotation[] getAnnotations() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public final Annotation[] getDeclaredAnnotations() { + throw VMError.intentionallyUnimplemented(); + } + + // endregion Unimplemented methods +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedObjectType.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedObjectType.java new file mode 100644 index 000000000000..58197d1b1c6e --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedObjectType.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.WordBase; + +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.serialization.VisibleForSerialization; + +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaType; + +public final class InterpreterResolvedObjectType extends InterpreterResolvedJavaType { + + private final InterpreterResolvedJavaType componentType; + private final int modifiers; + private final InterpreterResolvedObjectType superclass; + private final InterpreterResolvedObjectType[] interfaces; + + // Populated after analysis. + private InterpreterConstantPool constantPool; + + @Platforms(Platform.HOSTED_ONLY.class) private ResolvedJavaType originalType; + + private final String sourceFileName; + + public static class VTableHolder { + public InterpreterResolvedObjectType holder; + public InterpreterResolvedJavaMethod[] vtable; + + public VTableHolder(InterpreterResolvedObjectType holder, InterpreterResolvedJavaMethod[] vtable) { + this.holder = holder; + this.vtable = vtable; + } + } + + private VTableHolder vtableHolder = null; + + // Debugger side constructor, class is an opaque JavaConstant. + private InterpreterResolvedObjectType(String name, int modifiers, InterpreterResolvedJavaType componentType, InterpreterResolvedObjectType superclass, InterpreterResolvedObjectType[] interfaces, + InterpreterConstantPool constantPool, + JavaConstant clazzConstant, + boolean isWordType, String sourceFileName) { + super(name, clazzConstant, isWordType); + this.modifiers = modifiers; + this.componentType = componentType; + this.superclass = superclass; + this.interfaces = interfaces; + this.constantPool = constantPool; + this.sourceFileName = sourceFileName; + } + + // Interpreter side constructor. + private InterpreterResolvedObjectType(String name, int modifiers, InterpreterResolvedJavaType componentType, InterpreterResolvedObjectType superclass, InterpreterResolvedObjectType[] interfaces, + InterpreterConstantPool constantPool, + Class javaClass, + boolean isWordType) { + super(name, javaClass, isWordType); + assert isWordType == WordBase.class.isAssignableFrom(javaClass); + this.modifiers = modifiers; + this.superclass = superclass; + this.interfaces = interfaces; + this.componentType = componentType; + this.constantPool = constantPool; + this.sourceFileName = DynamicHub.fromClass(javaClass).getSourceFileName(); + } + + @Platforms(Platform.HOSTED_ONLY.class) + private InterpreterResolvedObjectType(ResolvedJavaType originalType, String name, int modifiers, InterpreterResolvedJavaType componentType, InterpreterResolvedObjectType superclass, + InterpreterResolvedObjectType[] interfaces, InterpreterConstantPool constantPool, + Class javaClass, + String sourceFileName) { + super(name, javaClass); + this.originalType = originalType; + this.modifiers = modifiers; + this.componentType = componentType; + this.superclass = superclass; + this.interfaces = interfaces; + this.constantPool = constantPool; + this.sourceFileName = sourceFileName; + } + + @Override + public String getSourceFileName() { + return sourceFileName; + } + + // Only used for BuildTimeInterpreterUniverse. + @Platforms(Platform.HOSTED_ONLY.class) + public static InterpreterResolvedObjectType createAtBuildTime(ResolvedJavaType originalType, String name, int modifiers, InterpreterResolvedJavaType componentType, + InterpreterResolvedObjectType superclass, InterpreterResolvedObjectType[] interfaces, InterpreterConstantPool constantPool, + Class javaClass, + String sourceFileName) { + return new InterpreterResolvedObjectType(originalType, name, modifiers, componentType, superclass, interfaces, constantPool, javaClass, sourceFileName); + } + + @VisibleForSerialization + public static InterpreterResolvedObjectType createForInterpreter(String name, int modifiers, InterpreterResolvedJavaType componentType, InterpreterResolvedObjectType superclass, + InterpreterResolvedObjectType[] interfaces, InterpreterConstantPool constantPool, + Class javaClass, + boolean isWordType) { + return new InterpreterResolvedObjectType(name, modifiers, componentType, superclass, interfaces, constantPool, javaClass, isWordType); + } + + @VisibleForSerialization + public static InterpreterResolvedObjectType createWithOpaqueClass(String name, int modifiers, InterpreterResolvedJavaType componentType, InterpreterResolvedObjectType superclass, + InterpreterResolvedObjectType[] interfaces, InterpreterConstantPool constantPool, + JavaConstant clazzConstant, + boolean isWordType, + String sourceFileName) { + return new InterpreterResolvedObjectType(name, modifiers, componentType, superclass, interfaces, constantPool, clazzConstant, isWordType, sourceFileName); + } + + public void setConstantPool(InterpreterConstantPool constantPool) { + VMError.guarantee(this == constantPool.getHolder()); + this.constantPool = MetadataUtil.requireNonNull(constantPool); + } + + public InterpreterConstantPool getConstantPool() { + assert !isArray(); + return constantPool; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public ResolvedJavaType getOriginalType() { + return originalType; + } + + @Override + public int getModifiers() { + return modifiers; + } + + @Override + public InterpreterResolvedJavaType getComponentType() { + return componentType; + } + + @Override + public JavaKind getJavaKind() { + return JavaKind.Object; + } + + @Override + public InterpreterResolvedObjectType getSuperclass() { + return this.superclass; + } + + @Override + public InterpreterResolvedObjectType[] getInterfaces() { + return this.interfaces; + } + + @Override + public boolean isAssignableFrom(ResolvedJavaType other) { + if (other instanceof InterpreterResolvedObjectType o) { + return isSubTypeOf(this, o); + } + return false; + } + + private static boolean isSubTypeOf(InterpreterResolvedObjectType superType, InterpreterResolvedObjectType subType) { + if (subType.equals(superType)) { + return true; + } + if (subType.superclass != null) { + if (isSubTypeOf(superType, subType.superclass)) { + return true; + } + } + for (InterpreterResolvedObjectType interf : subType.interfaces) { + if (isSubTypeOf(superType, interf)) { + return true; + } + } + return false; + } + + public InterpreterResolvedJavaMethod[] getVtable() { + if (vtableHolder == null) { + return null; + } + return vtableHolder.vtable; + } + + public void setVtable(InterpreterResolvedJavaMethod[] vtable) { + VMError.guarantee(vtableHolder == null); + this.vtableHolder = new VTableHolder(this, vtable); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public VTableHolder getVtableHolder() { + assert !isArray(); + return vtableHolder; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedPrimitiveType.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedPrimitiveType.java new file mode 100644 index 000000000000..23052e89d08a --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterResolvedPrimitiveType.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import java.lang.reflect.Modifier; + +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaType; + +public final class InterpreterResolvedPrimitiveType extends InterpreterResolvedJavaType { + + private final JavaKind kind; + + private InterpreterResolvedPrimitiveType(JavaKind kind) { + super(String.valueOf(kind.getTypeChar()), kind.toJavaClass()); + assert kind.isPrimitive(); + this.kind = kind; + } + + static final InterpreterResolvedPrimitiveType BOOLEAN = new InterpreterResolvedPrimitiveType(JavaKind.Boolean); + static final InterpreterResolvedPrimitiveType BYTE = new InterpreterResolvedPrimitiveType(JavaKind.Byte); + static final InterpreterResolvedPrimitiveType SHORT = new InterpreterResolvedPrimitiveType(JavaKind.Short); + static final InterpreterResolvedPrimitiveType CHAR = new InterpreterResolvedPrimitiveType(JavaKind.Char); + static final InterpreterResolvedPrimitiveType INT = new InterpreterResolvedPrimitiveType(JavaKind.Int); + static final InterpreterResolvedPrimitiveType FLOAT = new InterpreterResolvedPrimitiveType(JavaKind.Float); + static final InterpreterResolvedPrimitiveType LONG = new InterpreterResolvedPrimitiveType(JavaKind.Long); + static final InterpreterResolvedPrimitiveType DOUBLE = new InterpreterResolvedPrimitiveType(JavaKind.Double); + static final InterpreterResolvedPrimitiveType VOID = new InterpreterResolvedPrimitiveType(JavaKind.Void); + + public static InterpreterResolvedPrimitiveType fromKind(JavaKind kind) { + // @formatter:off + switch (kind) { + case Boolean : return BOOLEAN; + case Byte : return BYTE; + case Short : return SHORT; + case Char : return CHAR; + case Int : return INT; + case Float : return FLOAT; + case Long : return LONG; + case Double : return DOUBLE; + case Void : return VOID; + case Object : // fall-through + case Illegal : // fall-through + default: + throw new IllegalArgumentException(kind.toString()); + } + // @formatter:on + } + + @Override + public int getModifiers() { + return Modifier.ABSTRACT | Modifier.FINAL | Modifier.PUBLIC; + } + + @Override + public ResolvedJavaType getComponentType() { + return null; + } + + @Override + public JavaKind getJavaKind() { + return kind; + } + + @Override + public String getSourceFileName() { + return null; + } + + @Override + public ResolvedJavaType getSuperclass() { + return null; + } + + @Override + public ResolvedJavaType[] getInterfaces() { + return new ResolvedJavaType[0]; + } + + @Override + public boolean isAssignableFrom(ResolvedJavaType other) { + return this.equals(other); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUniverse.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUniverse.java new file mode 100644 index 000000000000..6acd297cc779 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUniverse.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import java.util.Collection; +import java.util.OptionalInt; + +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +public interface InterpreterUniverse { + Collection getTypes(); + + Collection getFields(); + + Collection getMethods(); + + ResolvedJavaType lookupType(Class clazz); + + Class lookupClass(ResolvedJavaType type); + + Collection getAllDeclaredFields(ResolvedJavaType type); + + Collection getAllDeclaredMethods(ResolvedJavaType type); + + OptionalInt getTypeIndexFor(ResolvedJavaType type); + + OptionalInt getFieldIndexFor(ResolvedJavaField field); + + OptionalInt getMethodIndexFor(ResolvedJavaMethod method); + + ResolvedJavaType getTypeAtIndex(int index); + + ResolvedJavaField getFieldAtIndex(int index); + + ResolvedJavaMethod getMethodAtIndex(int index); + + ResolvedJavaMethod getMethodForESTOffset(int methodIndex); + + ResolvedJavaMethod getMethodFromMethodId(int methodId); +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUniverseImpl.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUniverseImpl.java new file mode 100644 index 000000000000..2d1022032acc --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUniverseImpl.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.EST_NO_ENTRY; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.zip.CRC32C; + +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.serialization.SerializationContext; + +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * Classes, fields, methods and constants for the interpreter, the data structures in this universe + * are alive at runtime. + *

+ * This class can be used in SVM and HotSpot, in the resident and the server, and can load the + * interpreter universe using opaque constants even the image heap is not accessible. + */ +public final class InterpreterUniverseImpl implements InterpreterUniverse { + + static final int MAGIC = 0xE597E550; // ESPRESSO + private static final boolean LOGGING = false; + + private final List types; + private final List fields; + private final List methods; + private final Lazy, InterpreterResolvedJavaType>> classToType = // + Lazy.of(() -> getTypes().stream() + // Class -> type + .collect(Collectors.toMap(InterpreterResolvedJavaType::getJavaClass, Function.identity()))); + + private final Lazy>> allDeclaredFields = // + Lazy.of(() -> getFields().stream() + // type -> [field...] + .collect(Collectors.groupingBy(InterpreterResolvedJavaField::getDeclaringClass))); + private final Lazy>> allDeclaredMethods = // + Lazy.of(() -> getMethods().stream() + // type -> [method...] + .collect(Collectors.groupingBy(InterpreterResolvedJavaMethod::getDeclaringClass))); + + private final Lazy methodESTOffsetTable = Lazy.of(() -> createMethodTable(getMethods())); + + private final Lazy> methodInverseTable = Lazy.of(() -> createInverseTable(getMethods())); + private final Lazy> typeInverseTable = Lazy.of(() -> createInverseTable(getTypes())); + + private final Lazy> fieldInverseTable = Lazy.of(() -> createInverseTable(getFields())); + + private final Lazy methodIdMapping = Lazy.of(() -> createMethodIdMapping(getMethods())); + + public InterpreterUniverseImpl( + Collection types, + Collection fields, + Collection methods) { + this.types = List.copyOf(types); + this.fields = List.copyOf(fields); + this.methods = List.copyOf(methods); + } + + private static void consumeMagic(DataInput in) throws IOException { + int header = in.readInt(); + if (header != MAGIC) { + throw new IllegalStateException("Invalid header"); + } + } + + /** + * Serializes {@code this} {@link InterpreterUniverse} instance into the specified filePath. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public void saveTo(SerializationContext.Builder builder, Path filePath) throws IOException { + + int totalBytesWritten = 0; + + long startNanos = System.nanoTime(); + try (DataOutputStream out = new DataOutputStream( + new BufferedOutputStream( + Files.newOutputStream(filePath, + StandardOpenOption.WRITE, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING)))) { + + out.writeInt(MAGIC); + + SerializationContext.Writer writer = builder.buildWriter(); + + for (InterpreterResolvedJavaType type : getTypes()) { + writer.writeValue(out, type); + } + for (InterpreterResolvedJavaField field : getFields()) { + writer.writeValue(out, field); + } + for (InterpreterResolvedJavaMethod method : getMethods()) { + writer.writeValue(out, method); + } + for (InterpreterResolvedJavaMethod method : getMethods()) { + writer.writeValue(out, method.inlinedBy); + InterpreterResolvedJavaMethod oneImplementation = method.getOneImplementation(); + writer.writeReference(out, oneImplementation); + } + + for (InterpreterResolvedJavaType type : getTypes()) { + if (type instanceof InterpreterResolvedObjectType objectType && !type.isArray()) { + writer.writeValue(out, objectType.getConstantPool()); + if (objectType.getVtableHolder() != null) { + writer.writeValue(out, objectType.getVtableHolder()); + } + } + } + totalBytesWritten = out.size(); + } + long elapsedNanos = System.nanoTime() - startNanos; + if (LOGGING) { + long shallowInterpreterMethods = getMethods().stream().filter(Predicate.not(InterpreterResolvedJavaMethod::isInterpreterExecutable)).count(); + System.err.println("Save interpreter metadata:" + + " totalBytesWritten=" + totalBytesWritten + + " types=" + getTypes().size() + + " fields=" + getFields().size() + + " methods=" + getMethods().size() + + " (shallow=" + shallowInterpreterMethods + ")" + + " elapsedMillis=" + (elapsedNanos / 1_000_000)); + } + } + + public static String toHexString(int value) { + String result = Integer.toHexString(value); + return "0".repeat(8 - result.length()) + result; + } + + public static int computeCRC32(Path filePath) throws IOException { + CRC32C checksum = new CRC32C(); + try (InputStream in = new BufferedInputStream(Files.newInputStream(filePath, StandardOpenOption.READ))) { + byte[] buf = new byte[16 * (1 << 10)]; // 16KB + int bytesRead = 0; + while ((bytesRead = in.read(buf)) > 0) { + checksum.update(buf, 0, bytesRead); + } + } + return (int) checksum.getValue(); + } + + public static InterpreterUniverseImpl loadFrom(SerializationContext.Builder builder, boolean opaqueConstants, String hashString, Path filePath) throws IOException { + long startNanos = System.nanoTime(); + if (LOGGING) { + System.err.println("Interpreter metadata hash string: " + hashString); + } + + if (hashString != null) { + String[] parts = hashString.split(":"); + String algorithm = parts[0]; + String expectedHexChecksum = parts[1]; + + /* + * TODO(peterssen): GR-46008 Use secure hash to verify interpreter metadata. This check + * is meant ONLY to improve the user experience, not for security e.g. a secure hash + * should be used, also note that this check don't guard against the file being modified + * between the checksum/hash computation and actual de-serialization. + */ + if ("crc32".equalsIgnoreCase(algorithm)) { + int crc32 = computeCRC32(filePath); + String actualHexChecksum = toHexString(crc32); + if (!expectedHexChecksum.equals(actualHexChecksum)) { + throw new IllegalArgumentException( + "Invalid " + algorithm + " verification for metadata file: " + filePath + + " expected: " + expectedHexChecksum + " actual: " + actualHexChecksum); + } + } else { + throw VMError.unimplemented("Metadata verification with: " + algorithm); + } + } + + try (DataInputStream dataInput = new DataInputStream( + new BufferedInputStream( + Files.newInputStream(filePath, StandardOpenOption.READ)))) { + List types = new ArrayList<>(); + List fields = new ArrayList<>(); + List methods = new ArrayList<>(); + + consumeMagic(dataInput); + SerializationContext.Reader reader; + + if (opaqueConstants) { + // Configure ReferenceConstant de-serialization to return opaque constants (heap + // offsets) to the main application heap. + reader = builder + .registerReader(true, ReferenceConstant.class, (context, in) -> { + boolean inNativeHeap = in.readBoolean(); + if (inNativeHeap) { + long nativeHeapOffset = in.readLong(); + return ReferenceConstant.createFromHeapOffset(nativeHeapOffset); + } else { + Object ref = context.readReference(in); + return ReferenceConstant.createFromReference(ref); + } + }) + .buildReader(); + } else { + // nothing to change, de-serialization is already configured for the interpreter + // (constants are on the same heap). + reader = builder.buildReader(); + } + + while (true) { + try { + Object value = reader.readValue(dataInput); + if (value instanceof InterpreterResolvedJavaType type) { + types.add(type); + } else if (value instanceof InterpreterResolvedJavaField field) { + fields.add(field); + } else if (value instanceof InterpreterResolvedJavaMethod method) { + methods.add(method); + } else if (value instanceof InterpreterConstantPool constantPool) { + InterpreterResolvedObjectType holder = constantPool.getHolder(); + VMError.guarantee(!holder.isArray()); + holder.setConstantPool(constantPool); + } else if (value instanceof InterpreterResolvedObjectType.VTableHolder vTableHolder) { + InterpreterResolvedObjectType holder = vTableHolder.holder; + holder.setVtable(vTableHolder.vtable); + } else if (value instanceof InterpreterResolvedJavaMethod.InlinedBy inlinedBy) { + InterpreterResolvedJavaMethod holder = inlinedBy.holder; + for (InterpreterResolvedJavaMethod m : inlinedBy.inliners) { + holder.addInliner(m); + } + Object oneImpl = reader.readReference(dataInput); + if (oneImpl instanceof InterpreterResolvedJavaMethod interpreterResolvedJavaMethod) { + holder.setOneImplementation(interpreterResolvedJavaMethod); + } else { + VMError.guarantee(oneImpl == null); + } + } else { + // skip value + } + } catch (EOFException e) { + break; + } + } + + InterpreterUniverseImpl universe = new InterpreterUniverseImpl(types, fields, methods); + long elapsedNanos = System.nanoTime() - startNanos; + + if (LOGGING) { + System.err.println("Load interpreter metadata:" + + " types=" + universe.getTypes().size() + + " fields=" + universe.getFields().size() + + " methods=" + universe.getMethods().size() + + " elapsedMillis=" + (elapsedNanos / 1_000_000)); + } + + return universe; + } + } + + @Override + public List getTypes() { + return types; + } + + @Override + public List getFields() { + return fields; + } + + @Override + public List getMethods() { + return methods; + } + + @SuppressWarnings("static-method") + @Override + public Class lookupClass(ResolvedJavaType type) { + return ((InterpreterResolvedJavaType) type).getJavaClass(); + } + + @Override + public InterpreterResolvedJavaType lookupType(Class clazz) { + Map, InterpreterResolvedJavaType> map = classToType.get(); + InterpreterResolvedJavaType type = map.get(clazz); + assert type != null; + return type; + } + + @Override + public Collection getAllDeclaredFields(ResolvedJavaType type) { + InterpreterResolvedJavaType interpreterType = (InterpreterResolvedJavaType) type; + return allDeclaredFields.get().getOrDefault(interpreterType, List.of()); + } + + @Override + public Collection getAllDeclaredMethods(ResolvedJavaType type) { + InterpreterResolvedJavaType interpreterType = (InterpreterResolvedJavaType) type; + return allDeclaredMethods.get().getOrDefault(interpreterType, List.of()); + } + + private static InterpreterResolvedJavaMethod[] createMethodTable(Collection methods) { + int maxOffset = -1; + for (InterpreterResolvedJavaMethod m : methods) { + int methodOffset = m.getEnterStubOffset(); + if (methodOffset != EST_NO_ENTRY) { + VMError.guarantee(methodOffset >= 0); + maxOffset = Math.max(maxOffset, methodOffset); + } + } + InterpreterResolvedJavaMethod[] result = new InterpreterResolvedJavaMethod[maxOffset + 1]; + for (InterpreterResolvedJavaMethod m : methods) { + int methodOffset = m.getEnterStubOffset(); + if (methodOffset != EST_NO_ENTRY) { + VMError.guarantee(result[methodOffset] == null, "duplicated interpreter method entry"); + result[methodOffset] = m; + } + } + return result; + } + + @Override + public InterpreterResolvedJavaMethod getMethodForESTOffset(int methodIndex) { + InterpreterResolvedJavaMethod method = this.methodESTOffsetTable.get()[methodIndex]; + VMError.guarantee(method != null); + return method; + } + + private static Map createInverseTable(Collection list) { + HashMap map = new HashMap<>(); + int counter = 0; + for (T e : list) { + map.put(e, counter++); + } + return map; + } + + private static InterpreterResolvedJavaMethod[] createMethodIdMapping(List methods) { + int maxMethodId = 0; + for (InterpreterResolvedJavaMethod method : methods) { + int methodId = method.getMethodId(); + assert methodId >= 0; + maxMethodId = Math.max(maxMethodId, methodId); + } + InterpreterResolvedJavaMethod[] mapping = new InterpreterResolvedJavaMethod[maxMethodId + 1]; + for (InterpreterResolvedJavaMethod method : methods) { + int methodId = method.getMethodId(); + if (methodId != 0) { + mapping[methodId] = method; + } + } + return mapping; + } + + @Override + public ResolvedJavaMethod getMethodAtIndex(int index) { + return methods.get(index); + } + + @Override + public ResolvedJavaType getTypeAtIndex(int index) { + return types.get(index); + } + + @Override + public ResolvedJavaField getFieldAtIndex(int index) { + return fields.get(index); + } + + @Override + public OptionalInt getMethodIndexFor(ResolvedJavaMethod method) { + Integer index = methodInverseTable.get().get(method); + if (index == null) { + return OptionalInt.empty(); + } + return OptionalInt.of(index); + } + + @Override + public OptionalInt getTypeIndexFor(ResolvedJavaType type) { + Integer index = typeInverseTable.get().get(type); + if (index == null) { + return OptionalInt.empty(); + } + return OptionalInt.of(index); + } + + @Override + public OptionalInt getFieldIndexFor(ResolvedJavaField field) { + Integer index = fieldInverseTable.get().get(field); + if (index == null) { + return OptionalInt.empty(); + } + return OptionalInt.of(index); + } + + @Override + public InterpreterResolvedJavaMethod getMethodFromMethodId(int methodId) { + if (methodId == 0) { + return null; + } + InterpreterResolvedJavaMethod[] mapping = methodIdMapping.get(); + if (0 <= methodId && methodId < mapping.length) { + return mapping[methodId]; + } else { + // No interpreter method known for this methodId. + return null; + } + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUnresolvedSignature.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUnresolvedSignature.java new file mode 100644 index 000000000000..8db477401ddf --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/InterpreterUnresolvedSignature.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.interpreter.metadata.serialization.VisibleForSerialization; + +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.Signature; +import jdk.vm.ci.meta.UnresolvedJavaType; + +/** + * Unresolved signature that doesn't contain any resolved type references, except for primitive + * types. Primitive types are always and can only be resolved types. + */ +public final class InterpreterUnresolvedSignature implements Signature { + + private final JavaType returnType; + private final JavaType[] parameterTypes; + + @Platforms(Platform.HOSTED_ONLY.class) private Signature originalSignature; + + private static boolean primitiveOrUnresolved(JavaType... types) { + for (JavaType type : types) { + if (!(type instanceof InterpreterResolvedPrimitiveType || type instanceof UnresolvedJavaType)) { + return false; + } + } + return true; + } + + private InterpreterUnresolvedSignature(JavaType returnType, JavaType[] parameterTypes) { + assert primitiveOrUnresolved(returnType); + assert parameterTypes.getClass() == JavaType[].class; + assert primitiveOrUnresolved(parameterTypes); + this.returnType = returnType; + this.parameterTypes = parameterTypes; + } + + @Platforms(Platform.HOSTED_ONLY.class) + private InterpreterUnresolvedSignature(Signature originalSignature, JavaType returnType, JavaType[] parameterTypes) { + this(returnType, parameterTypes); + this.originalSignature = originalSignature; + } + + @Platforms(Platform.HOSTED_ONLY.class) + @VisibleForSerialization + public static InterpreterUnresolvedSignature create(Signature originalSignature, JavaType returnType, JavaType[] parameterTypes) { + return new InterpreterUnresolvedSignature(originalSignature, returnType, parameterTypes); + } + + @VisibleForSerialization + public static InterpreterUnresolvedSignature create(JavaType returnType, JavaType[] parameterTypes) { + return new InterpreterUnresolvedSignature(returnType, parameterTypes); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public Signature getOriginalSignature() { + return originalSignature; + } + + @Override + public int getParameterCount(boolean receiver) { + int count = parameterTypes.length; + if (receiver) { + ++count; + } + return count; + } + + public int slotsForParameters(boolean receiver) { + int slots = 0; + + if (receiver) { + ++slots; + } + + for (JavaType parameterType : parameterTypes) { + slots += parameterType.getJavaKind().getSlotCount(); + } + return slots; + } + + @Override + public JavaType getParameterType(int index, ResolvedJavaType accessingClass) { + return parameterTypes[index]; + } + + @Override + public JavaType getReturnType(ResolvedJavaType accessingClass) { + return returnType; + } + + @Override + public String toString() { + return "InterpreterUnresolvedSignature<" + toMethodDescriptor() + ">"; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof InterpreterUnresolvedSignature that) { + return returnType.equals(that.returnType) && MetadataUtil.arrayEquals(parameterTypes, that.parameterTypes); + } else { + return false; + } + } + + @Override + public int hashCode() { + int result = MetadataUtil.hashCode(returnType); + result = 31 * result + MetadataUtil.arrayHashCode(parameterTypes); + return result; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/Lazy.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/Lazy.java new file mode 100644 index 000000000000..22e8bb038050 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/Lazy.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +/** + * Represents a lazily computed value. Ensures that a single thread runs the computation. + */ +public final class Lazy { + + @FunctionalInterface + public interface LazySupplier { + T get(); + } + + private volatile T ref; + private final LazySupplier supplier; + + private Lazy(LazySupplier supplier) { + this.supplier = supplier; + } + + /** + * If the supplier returns null, {@link NullPointerException} is thrown. Exceptions + * thrown by the supplier will be propagated. If the supplier returns a non-null object, it will + * be cached and the computation is considered finished. The supplier is guaranteed to run on a + * single thread. A successful computation ({@link LazySupplier#get()} returns a non-null + * object) is guaranteed to be executed only once. + * + * @return the computed object, guaranteed to be non-null + */ + public T get() { + T localRef = ref; + if (localRef == null) { + synchronized (this) { + localRef = ref; + if (localRef == null) { + localRef = MetadataUtil.requireNonNull(supplier.get()); + ref = localRef; + } + } + } + return localRef; + } + + /** + * (Not so) Lazy value that does not run a computation. + */ + public static Lazy value(T nonNullValue) { + Lazy result = new Lazy<>(null); + result.ref = MetadataUtil.requireNonNull(nonNullValue); + return result; + } + + /** + * @param supplier if the supplier returns null, {@link #get()} will throw + * {@link NullPointerException} + */ + public static Lazy of(LazySupplier supplier) { + return new Lazy<>(MetadataUtil.requireNonNull(supplier)); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/LookupSwitch.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/LookupSwitch.java new file mode 100644 index 000000000000..371148ffa85d --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/LookupSwitch.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.LOOKUPSWITCH; + +import com.oracle.svm.core.util.VMError; + +/** + * A utility for processing {@link Bytecodes#LOOKUPSWITCH} bytecodes. + */ +public final class LookupSwitch { + + private static final int OFFSET_TO_NUMBER_PAIRS = 4; + private static final int OFFSET_TO_FIRST_PAIR_MATCH = 8; + private static final int OFFSET_TO_FIRST_PAIR_OFFSET = 12; + private static final int PAIR_SIZE = 8; + + private LookupSwitch() { + throw VMError.shouldNotReachHereAtRuntime(); + } + + /** + * Gets the offset from the start of the switch instruction for the {@code i}'th switch target. + * + * @param i the switch target index + * @return the offset to the {@code i}'th switch target + */ + public static int offsetAt(byte[] code, int bci, int i) { + assert BytecodeStream.opcode(code, bci) == LOOKUPSWITCH; + return BytecodeStream.readInt(code, TableSwitch.getAlignedBci(bci) + OFFSET_TO_FIRST_PAIR_OFFSET + PAIR_SIZE * i); + } + + /** + * Gets the key at {@code i}'th switch target index. + * + * @param i the switch target index + * @return the key at {@code i}'th switch target index + */ + public static int keyAt(byte[] code, int bci, int i) { + assert BytecodeStream.opcode(code, bci) == LOOKUPSWITCH; + return BytecodeStream.readInt(code, TableSwitch.getAlignedBci(bci) + OFFSET_TO_FIRST_PAIR_MATCH + PAIR_SIZE * i); + } + + /** + * Gets the number of switch targets. + * + * @return the number of switch targets + */ + public static int numberOfCases(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == LOOKUPSWITCH; + return BytecodeStream.readInt(code, TableSwitch.getAlignedBci(bci) + OFFSET_TO_NUMBER_PAIRS); + } + + /** + * Gets the total size in bytes of the switch instruction. + * + * @return the total size in bytes of the switch instruction + */ + public static int size(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == LOOKUPSWITCH; + return TableSwitch.getAlignedBci(bci) + OFFSET_TO_FIRST_PAIR_MATCH + PAIR_SIZE * numberOfCases(code, bci) - bci; + } + + /** + * Gets the index of the instruction denoted by the {@code i}'th switch target. + * + * @param i index of the switch target + * @return the index of the instruction denoted by the {@code i}'th switch target + */ + public static int targetAt(byte[] code, int bci, int i) { + assert BytecodeStream.opcode(code, bci) == LOOKUPSWITCH; + return bci + offsetAt(code, bci, i); + } + + /** + * Gets the index of the instruction for the default switch target. + * + * @return the index of the instruction for the default switch target + */ + public static int defaultTarget(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == LOOKUPSWITCH; + return bci + defaultOffset(code, bci); + } + + /** + * Gets the offset from the start of the switch instruction to the default switch target. + * + * @return the offset to the default switch target + */ + public static int defaultOffset(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == LOOKUPSWITCH; + return BytecodeStream.readInt(code, TableSwitch.getAlignedBci(bci)); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/MetadataUtil.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/MetadataUtil.java new file mode 100644 index 000000000000..901aef3a3a6b --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/MetadataUtil.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; + +import jdk.vm.ci.meta.JavaField; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.Signature; + +/** + * Duplicated utilities from {@link Objects} and {@link Arrays} to remove the dependencies on the + * standard library e.g. avoid a breakpoint on {@link Objects#requireNonNull(Object)} to the affect + * the debugger/interpreter implementation. + */ +public final class MetadataUtil { + + public static boolean equals(Object a, Object b) { + return (a == b) || (a != null && a.equals(b)); + } + + public static T requireNonNull(T obj) { + obj.getClass(); // trigger implicit NPE + return obj; + } + + public static int hashCode(Object o) { + return o != null ? o.hashCode() : 0; + } + + public static int arrayHashCode(Object[] a) { + if (a == null) { + return 0; + } + + int result = 1; + + for (Object element : a) { + result = 31 * result + (element == null ? 0 : element.hashCode()); + } + + return result; + } + + public static boolean arrayEquals(Object[] a, Object[] a2) { + if (a == a2) { + return true; + } + if (a == null || a2 == null) { + return false; + } + + int length = a.length; + if (a2.length != length) { + return false; + } + + for (int i = 0; i < length; i++) { + if (!MetadataUtil.equals(a[i], a2[i])) { + return false; + } + } + + return true; + } + + /** + * Cheap alternative to {@link String#format(String, Object...)} that only provides simple + * modifiers. + */ + // GR-55171: Consolidate into LogUtils + public static String fmt(String simpleFormat, Object... args) throws IllegalArgumentException { + StringBuilder sb = new StringBuilder(); + int index = 0; + int argIndex = 0; + while (index < simpleFormat.length()) { + char ch = simpleFormat.charAt(index++); + if (ch == '%') { + if (index >= simpleFormat.length()) { + throw new IllegalArgumentException("An unquoted '%' character cannot terminate a format specification"); + } + char specifier = simpleFormat.charAt(index++); + switch (specifier) { + case 's' -> { + if (argIndex >= args.length) { + throw new IllegalArgumentException("Too many format specifiers or not enough arguments"); + } + sb.append(args[argIndex++]); + } + case '%' -> sb.append('%'); + case 'n' -> sb.append(System.lineSeparator()); + default -> throw new IllegalArgumentException("Illegal format specifier: " + specifier); + } + } else { + sb.append(ch); + } + } + if (argIndex < args.length) { + throw new IllegalArgumentException("Not enough format specifiers or too many arguments"); + } + return sb.toString(); + } + + public static String toUniqueString(Signature signature) { + return signature.toMethodDescriptor(); + } + + public static String toUniqueString(JavaMethod method) { + return fmt("%s.%s/%s", + toUniqueString(method.getDeclaringClass()), + method.getName(), + toUniqueString(method.getSignature())); + } + + public static String toUniqueString(JavaType type) { + return type.getName(); + } + + public static String toUniqueString(JavaField field) { + return fmt("%s.%s/%s", + toUniqueString(field.getDeclaringClass()), + field.getName(), + toUniqueString(field.getType())); + } + + private static final String METADATA_SUFFIX = ".metadata"; + + public static String metadataFileName(String binaryFileName) { + assert !binaryFileName.isEmpty(); + assert !binaryFileName.endsWith(File.pathSeparator); + return binaryFileName + METADATA_SUFFIX; + } + + @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "path.getParent() is never null") + public static Path metadataFilePath(Path binaryFilePath) { + String binaryFileName = binaryFilePath.getFileName().toString(); + String metadataFileName = metadataFileName(binaryFileName); + return binaryFilePath.resolveSibling(metadataFileName); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/ReferenceConstant.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/ReferenceConstant.java new file mode 100644 index 000000000000..794bb76d666f --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/ReferenceConstant.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import com.oracle.graal.pointsto.heap.ImageHeapConstant; +import com.oracle.svm.interpreter.metadata.serialization.VisibleForSerialization; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import java.util.Objects; + +public final class ReferenceConstant implements JavaConstant { + + private final T ref; // can be null + + // Opaque, used to identify constant on the image heap. + private final long imageHeapOffset; + + private ReferenceConstant(T ref) { + this.imageHeapOffset = 0L; + this.ref = ref; + } + + private ReferenceConstant(long heapOffset) { + this.imageHeapOffset = heapOffset; + this.ref = null; + } + + @VisibleForSerialization + public static ReferenceConstant createFromNonNullReference(T ref) { + return new ReferenceConstant<>(MetadataUtil.requireNonNull(ref)); + } + + @VisibleForSerialization + public static ReferenceConstant createFromReference(T ref) { + if (ref == null) { + // Cannot return the nullReference() singleton here since this method is + // used for de-serialization where reference "identity" must be preserved. + return new ReferenceConstant<>(null); + } + return createFromNonNullReference(ref); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static ReferenceConstant createFromImageHeapConstant(ImageHeapConstant imageHeapConstant) { + assert !imageHeapConstant.isNull() : imageHeapConstant; + return new ReferenceConstant<>(Objects.requireNonNull(imageHeapConstant)); + } + + @VisibleForSerialization + public static ReferenceConstant createFromHeapOffset(long nativeHeapOffset) { + assert nativeHeapOffset != 0L; + return new ReferenceConstant<>(nativeHeapOffset); + } + + public T getReferent() { + if (isOpaque()) { + throw new UnsupportedOperationException("Cannot extract opaque referent"); + } + return ref; + } + + @Override + public String toString() { + return "ReferenceConstant<" + ref + ">"; + } + + public boolean isOpaque() { + return imageHeapOffset != 0L; + } + + @Override + public JavaKind getJavaKind() { + return JavaKind.Object; + } + + @Override + public boolean isNull() { + return ref == null && imageHeapOffset == 0L; + } + + @Override + public boolean isDefaultForKind() { + return isNull(); + } + + @Override + public Object asBoxedPrimitive() { + throw new IllegalArgumentException(); + } + + @Override + public int asInt() { + throw new IllegalArgumentException(); + } + + @Override + public boolean asBoolean() { + throw new IllegalArgumentException(); + } + + @Override + public long asLong() { + throw new IllegalArgumentException(); + } + + @Override + public float asFloat() { + throw new IllegalArgumentException(); + } + + @Override + public double asDouble() { + throw new IllegalArgumentException(); + } + + @Override + public int hashCode() { + return System.identityHashCode(ref) ^ (int) (imageHeapOffset ^ (imageHeapOffset >>> 32)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return other instanceof ReferenceConstant that && this.imageHeapOffset == that.imageHeapOffset && this.ref == that.ref; + } + + private static final ReferenceConstant NULL = new ReferenceConstant<>(null); + + @SuppressWarnings("unchecked") + public static ReferenceConstant nullReference() { + return (ReferenceConstant) NULL; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/SuppressFBWarnings.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/SuppressFBWarnings.java new file mode 100644 index 000000000000..0963e274f270 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/SuppressFBWarnings.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used to suppress SpotBugs warnings. + */ +@Retention(RetentionPolicy.CLASS) +public @interface SuppressFBWarnings { + /** + * @see "https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html" + */ + String[] value(); + + /** + * Reason why the warning is suppressed. Use a SpotBugs issue id where appropriate. + */ + String justification(); +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/TableSwitch.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/TableSwitch.java new file mode 100644 index 000000000000..b46bfe45c461 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/TableSwitch.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.TABLESWITCH; + +import com.oracle.svm.core.util.VMError; + +/** + * A utility for processing {@link Bytecodes#TABLESWITCH} bytecodes. + */ +public final class TableSwitch { + + private static final int OFFSET_TO_LOW_KEY = 4; + private static final int OFFSET_TO_HIGH_KEY = 8; + private static final int OFFSET_TO_FIRST_JUMP_OFFSET = 12; + private static final int JUMP_OFFSET_SIZE = 4; + + public static int getAlignedBci(int bci) { + return (bci + 4) & 0xfffffffc; + } + + private TableSwitch() { + throw VMError.shouldNotReachHereAtRuntime(); + } + + /** + * Gets the key at {@code i}'th switch target index. + * + * @param i the switch target index + * @return the key at {@code i}'th switch target index + */ + public static int keyAt(byte[] code, int bci, int i) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return lowKey(code, bci) + i; + } + + /** + * Gets the offset from the start of the switch instruction for the {@code i}'th switch target. + * + * @param i the switch target index + * @return the offset to the {@code i}'th switch target + */ + public static int offsetAt(byte[] code, int bci, int i) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return BytecodeStream.readInt(code, getAlignedBci(bci) + OFFSET_TO_FIRST_JUMP_OFFSET + JUMP_OFFSET_SIZE * i); + } + + /** + * Gets the number of switch targets. + * + * @return the number of switch targets + */ + public static int numberOfCases(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return highKey(code, bci) - lowKey(code, bci) + 1; + } + + /** + * Gets the total size in bytes of the switch instruction. + * + * @return the total size in bytes of the switch instruction + */ + public static int size(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return getAlignedBci(bci) + OFFSET_TO_FIRST_JUMP_OFFSET + JUMP_OFFSET_SIZE * numberOfCases(code, bci) - bci; + } + + /** + * Gets the index of the instruction denoted by the {@code i}'th switch target. + * + * @param i index of the switch target + * @return the index of the instruction denoted by the {@code i}'th switch target + */ + public static int targetAt(byte[] code, int bci, int i) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return bci + offsetAt(code, bci, i); + } + + /** + * Gets the index of the instruction for the default switch target. + * + * @return the index of the instruction for the default switch target + */ + public static int defaultTarget(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return bci + defaultOffset(code, bci); + } + + /** + * Gets the offset from the start of the switch instruction to the default switch target. + * + * @return the offset to the default switch target + */ + public static int defaultOffset(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return BytecodeStream.readInt(code, getAlignedBci(bci)); + } + + /** + * Gets the low key of the table switch. + * + * @return the low key + */ + public static int lowKey(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return BytecodeStream.readInt(code, getAlignedBci(bci) + OFFSET_TO_LOW_KEY); + } + + /** + * Gets the high key of the table switch. + * + * @return the high key + */ + public static int highKey(byte[] code, int bci) { + assert BytecodeStream.opcode(code, bci) == TABLESWITCH; + return BytecodeStream.readInt(code, getAlignedBci(bci) + OFFSET_TO_HIGH_KEY); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ForkedDataOutput.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ForkedDataOutput.java new file mode 100644 index 000000000000..5bd8fef89e5f --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ForkedDataOutput.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.VMError; + +@Platforms(Platform.HOSTED_ONLY.class) +final class ForkedDataOutput implements DataOutput, AutoCloseable { + private final DataOutput root; + private final ByteArrayOutputStream bytes; + private final DataOutput delegate; + + ForkedDataOutput(DataOutput root) { + this.root = getRoot(root); + this.bytes = new ByteArrayOutputStream(); + this.delegate = new DataOutputStream(bytes); + } + + private static DataOutput getRoot(DataOutput out) { + DataOutput root = out; + while (root instanceof ForkedDataOutput) { + root = ((ForkedDataOutput) root).root; + } + return root; + } + + @Override + public void close() throws IOException { + root.write(this.bytes.toByteArray()); + } + + @Override + public void write(int b) throws IOException { + delegate.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + delegate.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + delegate.write(b, off, len); + } + + @Override + public void writeBoolean(boolean v) throws IOException { + delegate.writeBoolean(v); + } + + @Override + public void writeByte(int v) throws IOException { + delegate.writeByte(v); + } + + @Override + public void writeShort(int v) throws IOException { + delegate.writeShort(v); + } + + @Override + public void writeChar(int v) throws IOException { + delegate.writeChar(v); + } + + @Override + public void writeInt(int v) throws IOException { + delegate.writeInt(v); + } + + @Override + public void writeLong(long v) throws IOException { + delegate.writeLong(v); + } + + @Override + public void writeFloat(float v) throws IOException { + delegate.writeFloat(v); + } + + @Override + public void writeDouble(double v) throws IOException { + delegate.writeDouble(v); + } + + @Override + public void writeBytes(String s) throws IOException { + throw VMError.shouldNotReachHereAtRuntime(); + } + + @Override + public void writeChars(String s) throws IOException { + throw VMError.shouldNotReachHereAtRuntime(); + } + + @Override + public void writeUTF(String s) throws IOException { + throw VMError.shouldNotReachHereAtRuntime(); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/LEB128.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/LEB128.java new file mode 100644 index 000000000000..729b4ba944fa --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/LEB128.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public final class LEB128 { + + public static int readUnsignedInt(DataInput in) throws IOException { + int result = 0; + for (int i = 0;; ++i) { + byte b = in.readByte(); + result |= (b & 0x7F) << (i * 7); + // The first 4 groups of 7 bits are guaranteed to fit (4 * 7 = 28 bits). + // That leaves room for only the 4 low-order bits from the 5th group (which has index 4) + if (i == 4 && (b & 0xF0) != 0) { + throw new ArithmeticException("Value is larger than 32-bits"); + } + if ((b & 0x80) == 0) { + return result; + } + } + } + + public static void writeUnsignedInt(DataOutput out, int value) throws IOException { + int tmp = value; + do { + int b = tmp & 0x7F; + tmp >>>= 7; + if (tmp != 0) { + b |= 0x80; + } + out.writeByte(b); + } while (tmp != 0); + } + + private static int zigZagEncodeSign(int value) { + return (value << 1) ^ (value >> 31); + } + + private static int zigZagDecodeSign(int value) { + return (value >>> 1) ^ -(value & 1); + } + + public static int readSignedInt(DataInput in) throws IOException { + return zigZagDecodeSign(readUnsignedInt(in)); + } + + public static void writeSignedInt(DataOutput out, int value) throws IOException { + writeUnsignedInt(out, zigZagEncodeSign(value)); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ReaderImpl.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ReaderImpl.java new file mode 100644 index 000000000000..7dd88c8d5ba6 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ReaderImpl.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.util.List; +import java.util.NoSuchElementException; + +final class ReaderImpl extends SerializationContextImpl implements SerializationContext.Reader { + + private final ValueReader.Resolver readerResolver; + + ReaderImpl(List> knownClasses, ValueReader.Resolver readerResolver) { + super(knownClasses); + this.readerResolver = readerResolver; + } + + @Override + public T indexToReference(int refIndex) { + if (refIndex == NULL_REFERENCE_INDEX) { + return null; + } + @SuppressWarnings("unchecked") + T ref = (T) indexToReference.get(refIndex); + if (ref == null) { + throw new IllegalStateException("Uninitialized reference at index " + refIndex); + } + return ref; + } + + @Override + public ValueReader readerFor(Class targetClass) { + if (targetClass.isPrimitive()) { + throw new IllegalArgumentException("Use in/out directly to read/write primitives"); + } + ValueReader reader = readerResolver.resolve(targetClass); + if (reader != null) { + return reader; + } + throw new NoSuchElementException("Cannot resolve ValueReader for " + targetClass); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Serialization.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Serialization.java new file mode 100644 index 000000000000..36e10cfdfad8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Serialization.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.EOFException; +import java.io.IOException; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +public final class Serialization { + + @Platforms(Platform.HOSTED_ONLY.class) + public static void serializeSingle(SerializationContext.Builder builder, DataOutput out, Object object) throws IOException { + SerializationContext.Writer context = builder.buildWriter(); + context.writeValue(out, object); + } + + public static Object deserializeSingle(SerializationContext.Builder builder, DataInput in) throws IOException { + SerializationContext.Reader context = builder.buildReader(); + Object latest = null; + while (true) { + try { + latest = context.readValue(in); + } catch (EOFException e) { + break; + } + } + + return latest; + } + +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/SerializationContext.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/SerializationContext.java new file mode 100644 index 000000000000..6c9528e8997e --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/SerializationContext.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.List; + +import org.graalvm.collections.EconomicMap; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +public interface SerializationContext { + + int UNKNOWN_REFERENCE_INDEX = Integer.MIN_VALUE; + int NULL_REFERENCE_INDEX = 0; // index 0 == null + int CLASS_REFERENCE_INDEX = 1; // index 1 == Class.class + + int recordReference(T value); + + interface Reader extends SerializationContext { + + T indexToReference(int refIndex); + + ValueReader readerFor(Class targetClass); + + default T readReference(DataInput in) throws IOException { + int refIndex = LEB128.readUnsignedInt(in); + return indexToReference(refIndex); + } + + default T readValue(DataInput in) throws IOException { + Class targetClass = readReference(in); + T value = readerFor(targetClass).read(this, in); + recordReference(value); + return value; + } + + } + + @Platforms(Platform.HOSTED_ONLY.class) + interface Writer extends SerializationContext { + ValueWriter writerFor(Class targetClass); + + int referenceToIndex(T value); + + default void writeReference(DataOutput out, T value) throws IOException { + int refIndex = referenceToIndex(value); + if (refIndex == UNKNOWN_REFERENCE_INDEX) { + writeValue(out, value); + refIndex = referenceToIndex(value); + if (refIndex == UNKNOWN_REFERENCE_INDEX) { + throw new IllegalStateException("Written object was not registered properly"); + } + } + LEB128.writeUnsignedInt(out, refIndex); + } + + @SuppressWarnings("unchecked") + default void writeValue(DataOutput out, T value) throws IOException { + Class targetClass = (Class) value.getClass(); + ValueWriter valueWriter = writerFor(targetClass); + try (ForkedDataOutput fork = new ForkedDataOutput(out)) { + writeReference(fork, targetClass); + valueWriter.write(this, fork, value); + } + recordReference(value); + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + static Builder newBuilder() { + return new Builder(); + } + + final class Builder { + + private List> knownClasses = List.of(); + + public List> getKnownClasses() { + return knownClasses; + } + + public Builder setKnownClasses(List> knownClasses) { + this.knownClasses = knownClasses; + return this; + } + + private final EconomicMap, ValueReader> valueReaders = EconomicMap.create(); + + @Platforms(Platform.HOSTED_ONLY.class) // + private final EconomicMap, ValueWriter> valueWriters = EconomicMap.create(); + + @Platforms(Platform.HOSTED_ONLY.class) + public Builder registerSerializer(boolean overrideExisting, Class targetClass, ValueSerializer valueSerializer) { + registerReader(overrideExisting, targetClass, valueSerializer.getReader()); + registerWriter(overrideExisting, targetClass, valueSerializer.getWriter()); + return this; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public Builder registerSerializer(Class targetClass, ValueSerializer valueSerializer) { + return registerSerializer(false, targetClass, valueSerializer); + } + + public Builder registerReader(boolean overrideExisting, Class targetClass, ValueReader valueReader) { + if (!overrideExisting && valueReaders.containsKey(targetClass)) { + throw new IllegalArgumentException("ValueReader already exists for " + targetClass); + } + valueReaders.put(targetClass, valueReader); + return this; + } + + public Builder registerReader(Class targetClass, ValueReader valueReader) { + return registerReader(false, targetClass, valueReader); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public Builder registerWriter(boolean overrideExisting, Class targetClass, ValueWriter valueWriter) { + if (!overrideExisting && valueWriters.containsKey(targetClass)) { + throw new IllegalArgumentException("ValueWriter already exists for " + targetClass); + } + valueWriters.put(targetClass, valueWriter); + return this; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public Builder registerWriter(Class targetClass, ValueWriter valueWriter) { + return registerWriter(false, targetClass, valueWriter); + } + + public SerializationContext.Reader buildReader() { + return new ReaderImpl(knownClasses, new ValueReader.Resolver() { + + @SuppressWarnings("unchecked") + @Override + public ValueReader resolve(Class targetClass) { + return (ValueReader) valueReaders.get(targetClass); + } + }); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public SerializationContext.Writer buildWriter() { + return new WriterImpl(knownClasses, new ValueWriter.Resolver() { + + @SuppressWarnings("unchecked") + @Override + public ValueWriter resolve(Class targetClass) { + return (ValueWriter) valueWriters.get(targetClass); + } + }); + } + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/SerializationContextImpl.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/SerializationContextImpl.java new file mode 100644 index 000000000000..dfaaeaf4cc84 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/SerializationContextImpl.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; + +abstract class SerializationContextImpl implements SerializationContext { + // The following two objects represent a bidirectional map between references and indices. + protected final List indexToReference; + protected final IdentityHashMap referenceToIndex; + + protected final List> knownClasses; + + protected SerializationContextImpl(List> knownClasses) { + if (knownClasses.contains(Class.class)) { + throw new IllegalArgumentException("Known classes cannot contain Class.class"); + } + + this.knownClasses = knownClasses; + this.referenceToIndex = new IdentityHashMap<>(); + this.indexToReference = new ArrayList<>(); + + indexToReference.add(null); // index 0 == null + + int classIndex = recordReference(Class.class); // index 1 == Class.class + assert classIndex == CLASS_REFERENCE_INDEX; + + for (Class clazz : knownClasses) { + recordReference(clazz); + } + } + + @Override + public int recordReference(T value) { + if (value == null) { + return NULL_REFERENCE_INDEX; + } + if (referenceToIndex.containsKey(value)) { + throw new IllegalStateException("Duplicated reference: " + value); + } + int refIndex = indexToReference.size(); + indexToReference.add(value); + referenceToIndex.put(value, refIndex); + return refIndex; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Serializers.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Serializers.java new file mode 100644 index 000000000000..2a2114214c61 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Serializers.java @@ -0,0 +1,771 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.lang.invoke.MethodType; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.function.IntFunction; +import java.util.function.ToLongFunction; + +import com.oracle.svm.interpreter.metadata.ReferenceConstant; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.Pointer; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.FunctionPointerHolder; +import com.oracle.svm.core.snippets.KnownIntrinsics; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterConstantPool; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaField; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedObjectType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedPrimitiveType; +import com.oracle.svm.interpreter.metadata.InterpreterUnresolvedSignature; + +import jdk.vm.ci.meta.ExceptionHandler; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.LineNumberTable; +import jdk.vm.ci.meta.Local; +import jdk.vm.ci.meta.LocalVariableTable; +import jdk.vm.ci.meta.PrimitiveConstant; +import jdk.vm.ci.meta.UnresolvedJavaField; +import jdk.vm.ci.meta.UnresolvedJavaMethod; +import jdk.vm.ci.meta.UnresolvedJavaType; + +/** + * Serializers for types included in the interpreter metadata. + */ +public final class Serializers { + + @VisibleForSerialization + public static ValueSerializer createSerializer(ValueReader reader, ValueWriter writer) { + return new ValueSerializer<>(reader, writer); + } + + public static final ValueSerializer BYTE_ARRAY = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + byte[] bytes = new byte[length]; + in.readFully(bytes); + return bytes; + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.length); + out.write(value); + }); + + public static final ValueSerializer STRING = createSerializer( + (context, in) -> { + byte[] bytes = BYTE_ARRAY.getReader().read(context, in); + return new String(bytes, StandardCharsets.UTF_8); + }, + (context, out, value) -> { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + BYTE_ARRAY.getWriter().write(context, out, bytes); + }); + + public static final ValueSerializer> CLASS_BY_NAME = createSerializer( + (context, in) -> { + boolean isPrimitive = in.readBoolean(); + if (isPrimitive) { + return JavaKind.fromPrimitiveOrVoidTypeChar(in.readChar()).toJavaClass(); + } else { + String name = STRING.getReader().read(context, in); + try { + return Class.forName(name); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + }, + (context, out, value) -> { + boolean isPrimitive = value.isPrimitive(); + out.writeBoolean(isPrimitive); + if (isPrimitive) { + out.writeChar(JavaKind.fromJavaClass(value).getTypeChar()); + } else { + STRING.getWriter().write(context, out, value.getName()); + } + }); + static final ValueSerializer BOOLEAN_ARRAY = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + boolean[] array = new boolean[length]; + for (int i = 0; i < length; ++i) { + array[i] = in.readBoolean(); + } + return array; + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.length); + for (boolean e : value) { + out.writeBoolean(e); + } + }); + + static final ValueSerializer INT_ARRAY = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + int[] array = new int[length]; + for (int i = 0; i < length; ++i) { + array[i] = in.readInt(); + } + return array; + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.length); + for (int e : value) { + out.writeInt(e); + } + }); + + static final ValueSerializer SHORT_ARRAY = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + short[] array = new short[length]; + for (int i = 0; i < length; ++i) { + array[i] = in.readShort(); + } + return array; + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.length); + for (short e : value) { + out.writeShort(e); + } + }); + + static final ValueSerializer CHAR_ARRAY = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + char[] array = new char[length]; + for (int i = 0; i < length; ++i) { + array[i] = in.readChar(); + } + return array; + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.length); + for (char e : value) { + out.writeChar(e); + } + }); + + static final ValueSerializer FLOAT_ARRAY = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + float[] array = new float[length]; + for (int i = 0; i < length; ++i) { + array[i] = in.readFloat(); + } + return array; + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.length); + for (float e : value) { + out.writeFloat(e); + } + }); + + static final ValueSerializer DOUBLE_ARRAY = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + double[] array = new double[length]; + for (int i = 0; i < length; ++i) { + array[i] = in.readDouble(); + } + return array; + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.length); + for (double e : value) { + out.writeDouble(e); + } + }); + + static final ValueSerializer LONG_ARRAY = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + long[] array = new long[length]; + for (int i = 0; i < length; ++i) { + array[i] = in.readLong(); + } + return array; + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.length); + for (long e : value) { + out.writeLong(e); + } + }); + + public static > ValueSerializer ofEnum(Class enumClass) { + return createSerializer( + (context, in) -> { + String name = context.readReference(in); + return Enum.valueOf(enumClass, name); + }, + (context, out, value) -> { + context.writeReference(out, value.name()); + }); + } + + public static ValueSerializer ofReferenceArray(IntFunction allocateArray) { + return createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + T[] array = allocateArray.apply(length); + for (int i = 0; i < length; i++) { + array[i] = context.readReference(in); + } + return array; + }, + (context, out, value) -> { + int length = value.length; + LEB128.writeUnsignedInt(out, length); + for (T e : value) { + context.writeReference(out, e); + } + }); + } + + private static final ValueSerializer AS_REFERENCE_CONSTANT = createSerializer( + (context, in) -> { + ReferenceConstant ref = context.readerFor(ReferenceConstant.class).read(context, in); + return ref.getReferent(); + }, + (context, out, value) -> { + context.writerFor(ReferenceConstant.class).write(context, out, ReferenceConstant.createFromNonNullReference(value)); + }); + + /** + * Force serialization of reference using the ReferenceConstant serializer within the context. + */ + @SuppressWarnings("unchecked") + static ValueSerializer asReferenceConstant() { + return (ValueSerializer) AS_REFERENCE_CONSTANT; + } + + public static final ValueReader> REFERENCE_CONSTANT_READER = (context, in) -> { + boolean inNativeHeap = in.readBoolean(); + if (inNativeHeap) { + long nativeHeapAddress = in.readLong(); + if (nativeHeapAddress == 0L) { + return ReferenceConstant.nullReference(); + } + Pointer heapBase = KnownIntrinsics.heapBase(); + Object ref = heapBase.add(WordFactory.unsigned(nativeHeapAddress)).toObject(); + return ReferenceConstant.createFromNonNullReference(ref); + } else { + // The reference could have been serialized despite not being on the native image heap. + // This may happen for some known types e.g. String. + // Or can be serialized as null. + Object ref = context.readReference(in); + return ReferenceConstant.createFromReference(ref); + } + }; + + @Platforms(Platform.HOSTED_ONLY.class) + public static ValueWriter> newReferenceConstantWriter(ToLongFunction getImageHeapOffset) { + return (context, out, value) -> { + Object ref = value.getReferent(); + long imageHeapOffset = ref == null + ? 0L + : getImageHeapOffset.applyAsLong(ref); + + // The reference was not included in the image heap. + boolean inImageHeap = (ref == null || imageHeapOffset != 0L); + assert !(ref == null) || inImageHeap; // ref == null => inImageHeap + out.writeBoolean(inImageHeap); + if (inImageHeap) { + out.writeLong(imageHeapOffset); + } else if (ref instanceof String) { + // Allow Strings to be serialized despite not being on the native heap. + context.writeReference(out, ref); + } else { + // Serialize as null. + context.writeReference(out, null); + } + }; + } + + public static ValueSerializer> newReferenceConstantSerializer(ToLongFunction getNativeHeapAddress) { + return createSerializer(REFERENCE_CONSTANT_READER, newReferenceConstantWriter(getNativeHeapAddress)); + } + + static final ValueSerializer JAVA_KIND = Serializers.ofEnum(JavaKind.class); + + static final ValueSerializer UNRESOLVED_TYPE = createSerializer( + (context, in) -> { + String name = context.readReference(in); + return UnresolvedJavaType.create(name); + }, + (context, out, value) -> { + context.writeReference(out, value.getName()); + }); + + static final ValueSerializer UNRESOLVED_METHOD = createSerializer( + (context, in) -> { + String name = context.readReference(in); + InterpreterUnresolvedSignature signature = context.readReference(in); + JavaType holder = context.readReference(in); + assert holder instanceof InterpreterResolvedPrimitiveType || holder instanceof UnresolvedJavaType; + return new UnresolvedJavaMethod(name, signature, holder); + }, + (context, out, value) -> { + context.writeReference(out, value.getName()); + assert value.getSignature() instanceof InterpreterUnresolvedSignature; + context.writeReference(out, value.getSignature()); + context.writeReference(out, value.getDeclaringClass()); + }); + + static final ValueSerializer UNRESOLVED_FIELD = createSerializer( + (context, in) -> { + JavaType holder = context.readReference(in); + assert holder instanceof InterpreterResolvedPrimitiveType || holder instanceof UnresolvedJavaType; + String name = context.readReference(in); + JavaType type = context.readReference(in); + assert type instanceof InterpreterResolvedPrimitiveType || type instanceof UnresolvedJavaType; + return new UnresolvedJavaField(holder, name, type); + }, + (context, out, value) -> { + context.writeReference(out, value.getDeclaringClass()); + context.writeReference(out, value.getName()); + context.writeReference(out, value.getType()); + }); + + static final ValueSerializer PRIMITIVE_TYPE = createSerializer( + (context, in) -> { + JavaKind kind = context.readReference(in); + return InterpreterResolvedPrimitiveType.fromKind(kind); + }, + (context, out, value) -> { + context.writeReference(out, value.getJavaKind()); + }); + + static final ValueSerializer UNRESOLVED_SIGNATURE = createSerializer( + (context, in) -> { + JavaType returnType = context.readReference(in); + int length = LEB128.readUnsignedInt(in); + JavaType[] parameterTypes = new JavaType[length]; + for (int i = 0; i < length; ++i) { + parameterTypes[i] = context.readReference(in); + } + return InterpreterUnresolvedSignature.create(returnType, parameterTypes); + }, + (context, out, value) -> { + JavaType returnType = value.getReturnType(null); + context.writeReference(out, returnType); + int length = value.getParameterCount(false); + LEB128.writeUnsignedInt(out, length); + for (int i = 0; i < length; ++i) { + JavaType parameterType = value.getParameterType(i, null); + context.writeReference(out, parameterType); + } + }); + + static final ValueSerializer METHOD_TYPE = createSerializer( + (context, in) -> { + Class returnType = context.readReference(in); + int length = LEB128.readUnsignedInt(in); + Class[] parameterTypes = new Class[length]; + for (int i = 0; i < length; ++i) { + parameterTypes[i] = context.readReference(in); + } + return MethodType.methodType(returnType, parameterTypes); + }, + (context, out, value) -> { + Class returnType = value.returnType(); + context.writeReference(out, returnType); + int length = value.parameterCount(); + LEB128.writeUnsignedInt(out, length); + for (int i = 0; i < length; ++i) { + Class parameterType = value.parameterType(i); + context.writeReference(out, parameterType); + } + }); + + static final ValueSerializer LOCAL = createSerializer( + (context, in) -> { + String name = context.readReference(in); + JavaType type = context.readReference(in); + int startBci = LEB128.readUnsignedInt(in); + int endBci = LEB128.readUnsignedInt(in); + int slot = LEB128.readUnsignedInt(in); + return new Local(name, type, startBci, endBci, slot); + }, + (context, out, value) -> { + context.writeReference(out, value.getName()); + context.writeReference(out, value.getType()); + LEB128.writeUnsignedInt(out, value.getStartBCI()); + LEB128.writeUnsignedInt(out, value.getEndBCI()); + LEB128.writeUnsignedInt(out, value.getSlot()); + }); + + static final ValueSerializer EXCEPTION_HANDLER = createSerializer( + (context, in) -> { + int startBCI = LEB128.readUnsignedInt(in); + int endBCI = LEB128.readUnsignedInt(in); + int catchBCI = LEB128.readUnsignedInt(in); + int catchTypeCPI = LEB128.readUnsignedInt(in); + JavaType catchType = context.readReference(in); + return new ExceptionHandler(startBCI, endBCI, catchBCI, catchTypeCPI, catchType); + }, + (context, out, value) -> { + LEB128.writeUnsignedInt(out, value.getStartBCI()); + LEB128.writeUnsignedInt(out, value.getEndBCI()); + LEB128.writeUnsignedInt(out, value.getHandlerBCI()); + LEB128.writeUnsignedInt(out, value.catchTypeCPI()); + context.writeReference(out, value.getCatchType()); + }); + + static final ValueSerializer LOCAL_VARIABLE_TABLE = createSerializer( + (context, in) -> { + Local[] locals = context.readerFor(Local[].class).read(context, in); + return new LocalVariableTable(locals); + }, + (context, out, value) -> { + context.writerFor(Local[].class).write(context, out, value.getLocals()); + }); + + static final int[] EMPTY_INT_ARRAY = new int[0]; + + static final ValueSerializer LINE_NUMBER_TABLE = createSerializer( + (context, in) -> { + int length = LEB128.readUnsignedInt(in); + if (length == 0) { + return new LineNumberTable(EMPTY_INT_ARRAY, EMPTY_INT_ARRAY); + } + int[] lineNumbers = new int[length]; + int[] bcis = new int[length]; + + int lastLineNumber = 0; + int lastBci = 0; + for (int i = 0; i < length; i++) { + int curLineNumber = lastLineNumber + LEB128.readSignedInt(in); + int curBci = lastBci + LEB128.readSignedInt(in); + lineNumbers[i] = curLineNumber; + bcis[i] = curBci; + + lastLineNumber = curLineNumber; + lastBci = curBci; + } + + return new LineNumberTable(lineNumbers, bcis); + }, + (context, out, value) -> { + int[] lines = value.getLineNumbers(); + int[] bcis = value.getBcis(); + VMError.guarantee(lines.length == bcis.length); + LEB128.writeUnsignedInt(out, lines.length); + int lastLineNumber = 0; + int lastBci = 0; + for (int i = 0; i < lines.length; i++) { + LEB128.writeSignedInt(out, lines[i] - lastLineNumber); + LEB128.writeSignedInt(out, bcis[i] - lastBci); + lastLineNumber = lines[i]; + lastBci = bcis[i]; + } + }); + + static final ValueSerializer PRIMITIVE_CONSTANT = createSerializer( + (context, in) -> { + JavaKind kind = context.readReference(in); + long rawValue = in.readLong(); + if (kind == JavaKind.Illegal) { + // JavaConstant.forPrimitive below throws for JavaKind.Illegal. + return JavaConstant.forIllegal(); + } + return JavaConstant.forPrimitive(kind, rawValue); + }, + (context, out, value) -> { + context.writeReference(out, value.getJavaKind()); + out.writeLong(value.getRawValue()); + }); + + // Register this serializer for JavaConstant.NULL_POINTER.getClass(). + static final ValueSerializer NULL_CONSTANT = createSerializer( + (context, in) -> { + return JavaConstant.NULL_POINTER; + }, + (context, out, value) -> { + // nop + }); + + static final ValueSerializer CONSTANT_POOL = createSerializer( + (context, in) -> { + InterpreterResolvedObjectType holder = context.readReference(in); + Object[] entries = context.readerFor(Object[].class).read(context, in); + return InterpreterConstantPool.create(holder, entries); + }, + (context, out, value) -> { + context.writeReference(out, value.getHolder()); + context.writerFor(Object[].class).write(context, out, value.getEntries()); + }); + + static final ValueSerializer RESOLVED_FIELD = createSerializer( + (context, in) -> { + String name = context.readReference(in); + InterpreterResolvedJavaType type = context.readReference(in); + InterpreterResolvedObjectType declaringClass = context.readReference(in); + int modifiers = LEB128.readUnsignedInt(in); + int offset = LEB128.readUnsignedInt(in); + JavaConstant constant = context.readReference(in); + return InterpreterResolvedJavaField.create(name, modifiers, type, declaringClass, offset, constant); + }, + (context, out, value) -> { + context.writeReference(out, value.getName()); + context.writeReference(out, value.getType()); + context.writeReference(out, value.getDeclaringClass()); + LEB128.writeUnsignedInt(out, value.getModifiers()); + LEB128.writeUnsignedInt(out, value.getOffset()); + if (value.isUnmaterializedConstant()) { + JavaConstant constant = value.getUnmaterializedConstant(); + context.writeReference(out, constant); + } else { + context.writeReference(out, null); + } + }); + + static final ValueSerializer OBJECT_TYPE = createSerializer( + (context, in) -> { + String name = context.readReference(in); + int modifiers = LEB128.readUnsignedInt(in); + InterpreterResolvedJavaType componentType = context.readReference(in); + + InterpreterResolvedObjectType superclass = context.readReference(in); + InterpreterResolvedObjectType[] interfaces = context.readReference(in); + /* vtable is serialized later to avoid cycle dependencies via methods */ + + InterpreterConstantPool constantPool = context.readReference(in); + ReferenceConstant> clazzConstant = context.readReference(in); + boolean isWordType = in.readBoolean(); + String sourceFileName = context.readReference(in); + if (clazzConstant.isOpaque()) { + return InterpreterResolvedObjectType.createWithOpaqueClass(name, modifiers, componentType, superclass, interfaces, constantPool, clazzConstant, isWordType, sourceFileName); + } else { + return InterpreterResolvedObjectType.createForInterpreter(name, modifiers, componentType, superclass, interfaces, constantPool, clazzConstant.getReferent(), isWordType); + } + }, + (context, out, value) -> { + String name = value.getName(); + int modifiers = value.getModifiers(); + InterpreterResolvedJavaType componentType = value.getComponentType(); + + InterpreterResolvedObjectType superclass = value.getSuperclass(); + InterpreterResolvedObjectType[] interfaces = value.getInterfaces(); + + // Constant pools are serialized separately, to break reference cycles, and + // patched after deserialization. + InterpreterConstantPool constantPool = null; + + Class javaClass = value.getJavaClass(); + ReferenceConstant> clazzConstant = ReferenceConstant.createFromNonNullReference(javaClass); + + context.writeReference(out, name); + LEB128.writeUnsignedInt(out, modifiers); + context.writeReference(out, componentType); + + context.writeReference(out, superclass); + context.writeReference(out, interfaces); + + context.writeReference(out, constantPool); + context.writeReference(out, clazzConstant); + out.writeBoolean(value.isWordType()); + context.writeReference(out, value.getSourceFileName()); + }); + + static final ValueSerializer VTABLE_HOLDER = createSerializer( + (context, in) -> { + InterpreterResolvedObjectType holder = context.readReference(in); + InterpreterResolvedJavaMethod[] vtable = context.readerFor(InterpreterResolvedJavaMethod[].class).read(context, in); + return new InterpreterResolvedObjectType.VTableHolder(holder, vtable); + }, + (context, out, value) -> { + context.writeReference(out, value.holder); + context.writerFor(InterpreterResolvedJavaMethod[].class).write(context, out, value.vtable); + }); + + static final ValueSerializer RESOLVED_METHOD = createSerializer( + (context, in) -> { + String name = context.readReference(in); + int maxLocals = LEB128.readUnsignedInt(in); + int maxStackSize = LEB128.readUnsignedInt(in); + int modifiers = LEB128.readUnsignedInt(in); + InterpreterResolvedObjectType declaringClass = context.readReference(in); + InterpreterUnresolvedSignature signature = context.readReference(in); + byte[] code = context.readReference(in); + ExceptionHandler[] exceptionHandlers = context.readReference(in); + LineNumberTable lineNumberTable = context.readReference(in); + LocalVariableTable localVariableTable = context.readReference(in); + + ReferenceConstant nativeEntryPoint = context.readReference(in); + int vtableIndex = LEB128.readUnsignedInt(in); + int gotOffset = LEB128.readUnsignedInt(in); + int enterStubOffset = LEB128.readUnsignedInt(in); + int methodId = LEB128.readUnsignedInt(in); + + return InterpreterResolvedJavaMethod.create(name, maxLocals, maxStackSize, modifiers, declaringClass, signature, code, exceptionHandlers, lineNumberTable, localVariableTable, + nativeEntryPoint, vtableIndex, gotOffset, enterStubOffset, methodId); + }, + (context, out, value) -> { + String name = value.getName(); + int maxLocals = value.getMaxLocals(); + int maxStackSize = value.getMaxStackSize(); + int modifiers = value.getModifiers(); + InterpreterResolvedObjectType declaringClass = value.getDeclaringClass(); + InterpreterUnresolvedSignature signature = value.getSignature(); + byte[] code = value.getInterpretedCode(); + ExceptionHandler[] exceptionHandlers = value.getExceptionHandlers(); + LineNumberTable lineNumberTable = value.getLineNumberTable(); + LocalVariableTable localVariableTable = value.getLocalVariableTable(); + /* + * `inlinedBy` and `oneImplementation` serialized separately to break + * reference cycle + */ + + ReferenceConstant nativeEntryPointHolder = value.getNativeEntryPointHolderConstant(); + int vtableIndex = value.getVTableIndex(); + int gotOffset = value.getGotOffset(); + int enterStubOffset = value.getEnterStubOffset(); + int methodId = value.getMethodId(); + + context.writeReference(out, name); + LEB128.writeUnsignedInt(out, maxLocals); + LEB128.writeUnsignedInt(out, maxStackSize); + LEB128.writeUnsignedInt(out, modifiers); + context.writeReference(out, declaringClass); + context.writeReference(out, signature); + context.writeReference(out, code); + context.writeReference(out, exceptionHandlers); + context.writeReference(out, lineNumberTable); + context.writeReference(out, localVariableTable); + + context.writeReference(out, nativeEntryPointHolder); + LEB128.writeUnsignedInt(out, vtableIndex); + LEB128.writeUnsignedInt(out, gotOffset); + LEB128.writeUnsignedInt(out, enterStubOffset); + LEB128.writeUnsignedInt(out, methodId); + }); + static final ValueSerializer INLINED_BY = createSerializer( + (context, in) -> { + InterpreterResolvedJavaMethod holder = context.readReference(in); + int size = LEB128.readUnsignedInt(in); + + InterpreterResolvedJavaMethod.InlinedBy ret = new InterpreterResolvedJavaMethod.InlinedBy(holder, new HashSet<>()); + while (size > 0) { + ret.inliners.add(context.readReference(in)); + size--; + } + return ret; + }, + (context, out, value) -> { + InterpreterResolvedJavaMethod holder = value.holder; + int size = value.inliners.size(); + + context.writeReference(out, holder); + LEB128.writeUnsignedInt(out, size); + for (InterpreterResolvedJavaMethod m : value.inliners) { + context.writeReference(out, m); + } + }); + + public static final List> UNIVERSE_KNOWN_CLASSES = List.of( + byte[].class, + String.class, + JavaKind.class, + UnresolvedJavaType.class, + UnresolvedJavaField.class, + UnresolvedJavaMethod.class, + InterpreterUnresolvedSignature.class, + Local.class, + Local[].class, + LocalVariableTable.class, + LineNumberTable.class, + ExceptionHandler.class, + ExceptionHandler[].class, + PrimitiveConstant.class, + JavaConstant.NULL_POINTER.getClass(), + MethodType.class, + InterpreterConstantPool.class, + Object[].class, + InterpreterResolvedObjectType[].class, + InterpreterResolvedJavaMethod[].class, + ReferenceConstant.class, + InterpreterResolvedPrimitiveType.class, + InterpreterResolvedObjectType.class, + InterpreterResolvedObjectType.VTableHolder.class, + InterpreterResolvedJavaField.class, + FunctionPointerHolder.class, + InterpreterResolvedJavaMethod.class, + InterpreterResolvedJavaMethod.InlinedBy.class); + + @Platforms(Platform.HOSTED_ONLY.class) + public static SerializationContext.Builder newBuilderForInterpreterMetadata() { + @SuppressWarnings("unchecked") + Class nullConstantClass = (Class) JavaConstant.NULL_POINTER.getClass(); + return SerializationContext.newBuilder() + .setKnownClasses(UNIVERSE_KNOWN_CLASSES) + // Only UNIVERSE_KNOWN_CLASSES can be (de-)serialized. + .registerSerializer(byte[].class, BYTE_ARRAY) + .registerSerializer(String.class, STRING) + .registerSerializer(JavaKind.class, JAVA_KIND) + .registerSerializer(UnresolvedJavaType.class, UNRESOLVED_TYPE) + .registerSerializer(UnresolvedJavaField.class, UNRESOLVED_FIELD) + .registerSerializer(UnresolvedJavaMethod.class, UNRESOLVED_METHOD) + .registerSerializer(InterpreterUnresolvedSignature.class, UNRESOLVED_SIGNATURE) + .registerSerializer(Local.class, LOCAL) + .registerSerializer(Local[].class, ofReferenceArray(Local[]::new)) + .registerSerializer(LocalVariableTable.class, LOCAL_VARIABLE_TABLE) + .registerSerializer(LineNumberTable.class, LINE_NUMBER_TABLE) + .registerSerializer(ExceptionHandler.class, EXCEPTION_HANDLER) + .registerSerializer(ExceptionHandler[].class, ofReferenceArray(ExceptionHandler[]::new)) + .registerSerializer(PrimitiveConstant.class, PRIMITIVE_CONSTANT) + .registerSerializer(nullConstantClass, NULL_CONSTANT) + .registerSerializer(MethodType.class, METHOD_TYPE) + .registerSerializer(InterpreterConstantPool.class, CONSTANT_POOL) + .registerSerializer(Object[].class, ofReferenceArray(Object[]::new)) // CP-entries + .registerSerializer(InterpreterResolvedObjectType[].class, ofReferenceArray(InterpreterResolvedObjectType[]::new)) // interfaces + .registerSerializer(InterpreterResolvedJavaMethod[].class, ofReferenceArray(InterpreterResolvedJavaMethod[]::new)) // vtable + .registerSerializer(InterpreterResolvedPrimitiveType.class, PRIMITIVE_TYPE) + .registerSerializer(InterpreterResolvedObjectType.class, OBJECT_TYPE) + .registerSerializer(InterpreterResolvedObjectType.VTableHolder.class, VTABLE_HOLDER) + .registerSerializer(InterpreterResolvedJavaField.class, RESOLVED_FIELD) + .registerSerializer(FunctionPointerHolder.class, asReferenceConstant()) + .registerSerializer(InterpreterResolvedJavaMethod.class, RESOLVED_METHOD) + .registerSerializer(InterpreterResolvedJavaMethod.InlinedBy.class, INLINED_BY) + .registerReader(ReferenceConstant.class, REFERENCE_CONSTANT_READER) + // The ReferenceConstant writer must be patched at build time. + .registerWriter(ReferenceConstant.class, (context, out, value) -> { + throw VMError.shouldNotReachHereAtRuntime(); + }); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Unsigned5.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Unsigned5.java new file mode 100644 index 000000000000..f485d5c4951c --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/Unsigned5.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public final class Unsigned5 { + // Math constants for the modified UNSIGNED5 coding of Pack200 + private static final int lg_H = 6; // log-base-2 of H (lg 64 == 6) + private static final int H = 1 << lg_H; // number of "high" bytes (64) + private static final int X = 1; // there is one excluded byte ('\0') + private static final int MAX_b = (1 << Byte.SIZE) - 1; // largest byte value + private static final int L = (MAX_b + 1) - X - H; + + private static final int MAX_LENGTH = 5; + + public static void writeUnsignedInt(DataOutput out, int value) throws IOException { + int sum = value; + for (int i = 0; i < MAX_LENGTH - 1 && Integer.compareUnsigned(sum, L) >= 0; ++i) { + sum -= L; + out.writeByte((byte) (X + L + (sum & 0x3F))); + sum >>>= lg_H; + } + out.writeByte((byte) (X + sum)); + } + + public static int readUnsignedInt(DataInput in) throws IOException { + int sum = 0; + for (int i = 0; i < MAX_LENGTH; ++i) { + int bi = in.readUnsignedByte(); + if (bi < X) { + throw new IllegalStateException("Invalid byte"); + } + sum += (bi - X) << (i * lg_H); + if (bi < X + L) { + break; + } + } + return sum; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueReader.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueReader.java new file mode 100644 index 000000000000..b160937df63d --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueReader.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.io.DataInput; +import java.io.IOException; + +@FunctionalInterface +public interface ValueReader { + T read(SerializationContext.Reader context, DataInput in) throws IOException; + + @FunctionalInterface + interface Resolver { + ValueReader resolve(Class targetClass); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueSerializer.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueSerializer.java new file mode 100644 index 000000000000..e0d0e78150f5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueSerializer.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +public final class ValueSerializer { + private final ValueReader reader; + + @Platforms(Platform.HOSTED_ONLY.class) // + private final ValueWriter writer; + + @Platforms(Platform.HOSTED_ONLY.class) + public ValueSerializer(ValueReader reader, ValueWriter writer) { + this.reader = reader; + this.writer = writer; + } + + public ValueReader getReader() { + return reader; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public ValueWriter getWriter() { + return writer; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueWriter.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueWriter.java new file mode 100644 index 000000000000..5cb3ca883c84 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/ValueWriter.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.io.DataOutput; +import java.io.IOException; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +@Platforms(Platform.HOSTED_ONLY.class) +@FunctionalInterface +public interface ValueWriter { + + void write(SerializationContext.Writer context, DataOutput out, T value) throws IOException; + + @Platforms(Platform.HOSTED_ONLY.class) + @FunctionalInterface + interface Resolver { + ValueWriter resolve(Class targetClass); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/VisibleForSerialization.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/VisibleForSerialization.java new file mode 100644 index 000000000000..2d31f325d679 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/VisibleForSerialization.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Denotes that the class, method or field has its visibility relaxed, so that it is more widely + * visible than otherwise necessary for serialization. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface VisibleForSerialization { +} diff --git a/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/WriterImpl.java b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/WriterImpl.java new file mode 100644 index 000000000000..812768e70951 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter.metadata/src/com/oracle/svm/interpreter/metadata/serialization/WriterImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.metadata.serialization; + +import java.io.DataOutput; +import java.io.IOException; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +@Platforms(Platform.HOSTED_ONLY.class) +final class WriterImpl extends SerializationContextImpl implements SerializationContext.Writer { + private final Set cycleDetector = Collections.newSetFromMap(new IdentityHashMap<>()); + + private final ValueWriter.Resolver writerResolver; + + WriterImpl(List> knownClasses, ValueWriter.Resolver writerResolver) { + super(knownClasses); + this.writerResolver = writerResolver; + } + + @Override + public ValueWriter writerFor(Class targetClass) { + if (targetClass.isPrimitive()) { + throw new IllegalArgumentException("Use in/out directly to read/write primitives"); + } + ValueWriter writer = writerResolver.resolve(targetClass); + if (writer != null) { + return writer; + } + throw new NoSuchElementException("Cannot resolve ValueWriter for " + targetClass); + } + + @Override + public int referenceToIndex(T value) { + if (value == null) { + return NULL_REFERENCE_INDEX; + } + return referenceToIndex.getOrDefault(value, UNKNOWN_REFERENCE_INDEX); + } + + @Override + public void writeValue(DataOutput out, T value) throws IOException { + if (value == null) { + throw new NullPointerException(); + } + if (cycleDetector.contains(value)) { + throw new IllegalStateException("Detected reference cycle during serialization for " + value); + } + cycleDetector.add(value); + Writer.super.writeValue(out, value); + cycleDetector.remove(value); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/AArch64InterpreterStubSection.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/AArch64InterpreterStubSection.java new file mode 100644 index 000000000000..1c6b8afb656b --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/AArch64InterpreterStubSection.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter; + +import static com.oracle.objectfile.ObjectFile.RelocationKind.AARCH64_R_AARCH64_ADD_ABS_LO12_NC; +import static com.oracle.objectfile.ObjectFile.RelocationKind.AARCH64_R_AARCH64_ADR_PREL_PG_HI21; +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.EST_NO_ENTRY; + +import java.util.Collection; + +import com.oracle.objectfile.ObjectFile; +import com.oracle.svm.core.config.ConfigurationValues; +import com.oracle.svm.core.graal.aarch64.SubstrateAArch64RegisterConfig; +import com.oracle.svm.core.graal.meta.KnownOffsets; +import com.oracle.svm.core.graal.meta.SubstrateRegisterConfig; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.core.graal.aarch64.AArch64InterpreterStubs; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.hosted.image.NativeImage; + +import jdk.graal.compiler.asm.Assembler; +import jdk.graal.compiler.asm.Label; +import jdk.graal.compiler.asm.aarch64.AArch64MacroAssembler; +import jdk.graal.compiler.core.common.LIRKind; +import jdk.vm.ci.code.Register; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +public class AArch64InterpreterStubSection extends InterpreterStubSection { + public AArch64InterpreterStubSection() { + this.target = ConfigurationValues.getTarget(); + this.registerConfig = new SubstrateAArch64RegisterConfig(SubstrateRegisterConfig.ConfigKind.NATIVE_TO_JAVA, null, target, true); + this.valueKindFactory = javaKind -> LIRKind.fromJavaKind(target.arch, javaKind); + } + + @Override + protected byte[] generateEnterStubs(Collection methods) { + AArch64MacroAssembler masm = new AArch64MacroAssembler(target); + + Label interpEnterStub = new Label(); + masm.bind(interpEnterStub); + + try (AArch64MacroAssembler.ScratchRegister jmpTargetRegister = masm.getScratchRegister()) { + Register jmpTarget = jmpTargetRegister.getRegister(); + + masm.setCodePatchingAnnotationConsumer(this::recordEnterStubForPatching); + /* + * use indirect jump to avoid problems around branch islands (displacement larger than + * +/-128MB) + */ + masm.adrpAdd(jmpTarget); + masm.jmp(jmpTarget); + } + + /* emit enter trampolines for each method that potentially runs in the interpreter */ + for (InterpreterResolvedJavaMethod method : methods) { + VMError.guarantee(method.getEnterStubOffset() != EST_NO_ENTRY); + InterpreterUtil.log("[svm_interp] Adding stub for %s", method); + recordEnterTrampoline(method, masm.position()); + + /* pass the method index, the reference is obtained in enterInterpreterStub */ + masm.mov(AArch64InterpreterStubs.TRAMPOLINE_ARGUMENT, method.getEnterStubOffset()); + + masm.jmp(interpEnterStub); + } + + return masm.close(true); + } + + @Override + public int getVTableStubSize() { + return 8; + } + + @Override + protected byte[] generateVtableEnterStubs(int maxVtableIndex) { + AArch64MacroAssembler masm = new AArch64MacroAssembler(target); + + Label interpEnterStub = new Label(); + masm.bind(interpEnterStub); + + try (AArch64MacroAssembler.ScratchRegister jmpTargetRegister = masm.getScratchRegister()) { + Register jmpTarget = jmpTargetRegister.getRegister(); + + masm.setCodePatchingAnnotationConsumer(this::recordEnterStubForPatching); + /* + * use indirect jump to avoid problems around branch islands (displacement larger than + * +/-128MB) + */ + masm.adrpAdd(jmpTarget); + masm.jmp(jmpTarget); + } + + masm.align(getVTableStubSize()); + + int vTableEntrySize = KnownOffsets.singleton().getVTableEntrySize(); + for (int index = 0; index < maxVtableIndex; index++) { + int stubStart = masm.position(); + int stubEnd = stubStart + getVTableStubSize(); + + int offset = index * vTableEntrySize; + + /* pass current vtable offset as hidden argument */ + masm.mov(AArch64InterpreterStubs.TRAMPOLINE_ARGUMENT, offset); + + masm.jmp(interpEnterStub); + + masm.align(getVTableStubSize()); + assert masm.position() == stubEnd; + } + + return masm.close(true); + + } + + private int resolverPatchOffset = -1; + + private void recordEnterStubForPatching(Assembler.CodeAnnotation a) { + if (resolverPatchOffset != -1) { + return; + } + + assert a instanceof AArch64MacroAssembler.AdrpAddMacroInstruction annotation; + AArch64MacroAssembler.AdrpAddMacroInstruction annotation = (AArch64MacroAssembler.AdrpAddMacroInstruction) a; + + resolverPatchOffset = annotation.instructionPosition; + } + + @Override + protected void markEnterStubPatch(ObjectFile.ProgbitsSectionImpl pltBuffer, ResolvedJavaMethod enterStub) { + pltBuffer.markRelocationSite(resolverPatchOffset, AARCH64_R_AARCH64_ADR_PREL_PG_HI21, NativeImage.localSymbolNameForMethod(enterStub), 0); + pltBuffer.markRelocationSite(resolverPatchOffset + 4, AARCH64_R_AARCH64_ADD_ABS_LO12_NC, NativeImage.localSymbolNameForMethod(enterStub), 0); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/AMD64InterpreterStubSection.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/AMD64InterpreterStubSection.java new file mode 100644 index 000000000000..79bae391b94b --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/AMD64InterpreterStubSection.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter; + +import java.util.Collection; + +import com.oracle.objectfile.ObjectFile; +import com.oracle.svm.core.config.ConfigurationValues; +import com.oracle.svm.core.graal.amd64.AMD64InterpreterStubs; +import com.oracle.svm.core.graal.amd64.SubstrateAMD64RegisterConfig; +import com.oracle.svm.core.graal.meta.KnownOffsets; +import com.oracle.svm.core.graal.meta.SubstrateRegisterConfig; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.hosted.image.NativeImage; + +import jdk.graal.compiler.asm.Assembler; +import jdk.graal.compiler.asm.Label; +import jdk.graal.compiler.asm.amd64.AMD64BaseAssembler; +import jdk.graal.compiler.asm.amd64.AMD64MacroAssembler; +import jdk.graal.compiler.core.common.LIRKind; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.EST_NO_ENTRY; + +public class AMD64InterpreterStubSection extends InterpreterStubSection { + public AMD64InterpreterStubSection() { + this.target = ConfigurationValues.getTarget(); + this.registerConfig = new SubstrateAMD64RegisterConfig(SubstrateRegisterConfig.ConfigKind.NATIVE_TO_JAVA, null, target, true); + this.valueKindFactory = javaKind -> LIRKind.fromJavaKind(target.arch, javaKind); + } + + @Override + protected byte[] generateEnterStubs(Collection methods) { + AMD64MacroAssembler masm = new AMD64MacroAssembler(target); + + Label interpEnterStub = new Label(); + masm.bind(interpEnterStub); + + masm.setCodePatchingAnnotationConsumer(this::recordEnterStubForPatching); + masm.jmp(); + + /* emit enter trampolines for each method that potentially runs in the interpreter */ + for (InterpreterResolvedJavaMethod method : methods) { + VMError.guarantee(method.getEnterStubOffset() != EST_NO_ENTRY); + InterpreterUtil.log("[svm_interp] Adding stub for %s", method); + recordEnterTrampoline(method, masm.position()); + + /* pass the method index, the reference is obtained in enterInterpreterStub */ + masm.movq(AMD64InterpreterStubs.TRAMPOLINE_ARGUMENT, method.getEnterStubOffset()); + + masm.jmp(interpEnterStub); + } + + return masm.close(true); + } + + @Override + public int getVTableStubSize() { + return 16; + } + + @Override + protected byte[] generateVtableEnterStubs(int maxVtableIndex) { + AMD64MacroAssembler masm = new AMD64MacroAssembler(target); + + Label interpEnterStub = new Label(); + masm.bind(interpEnterStub); + + masm.setCodePatchingAnnotationConsumer(this::recordEnterStubForPatching); + masm.jmp(); + + masm.align(getVTableStubSize()); + + int vTableEntrySize = KnownOffsets.singleton().getVTableEntrySize(); + for (int index = 0; index < maxVtableIndex; index++) { + int stubStart = masm.position(); + int stubEnd = stubStart + getVTableStubSize(); + + int offset = index * vTableEntrySize; + + /* pass current vtable offset as hidden argument */ + masm.movq(AMD64InterpreterStubs.TRAMPOLINE_ARGUMENT, offset); + + masm.jmp(interpEnterStub); + + masm.align(getVTableStubSize()); + assert masm.position() == stubEnd; + } + + return masm.close(true); + } + + private int resolverPatchOffset; + private int resolverKindAddend; + private ObjectFile.RelocationKind resolverPatchRelocationKind = null; + + private void recordEnterStubForPatching(Assembler.CodeAnnotation a) { + if (resolverPatchRelocationKind != null) { + return; + } + + assert a instanceof AMD64BaseAssembler.OperandDataAnnotation; + AMD64BaseAssembler.OperandDataAnnotation annotation = (AMD64BaseAssembler.OperandDataAnnotation) a; + resolverPatchOffset = annotation.operandPosition; + resolverKindAddend = -annotation.operandSize; + resolverPatchRelocationKind = ObjectFile.RelocationKind.getPCRelative(annotation.operandSize); + } + + @Override + protected void markEnterStubPatch(ObjectFile.ProgbitsSectionImpl pltBuffer, ResolvedJavaMethod enterStub) { + pltBuffer.markRelocationSite(resolverPatchOffset, resolverPatchRelocationKind, NativeImage.localSymbolNameForMethod(enterStub), resolverKindAddend); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/BuildTimeConstantPool.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/BuildTimeConstantPool.java new file mode 100644 index 000000000000..1601ee015e0f --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/BuildTimeConstantPool.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.ANEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.CHECKCAST; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GETFIELD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GETSTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INSTANCEOF; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEDYNAMIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEINTERFACE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESPECIAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEVIRTUAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC2_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.MULTIANEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.NEW; +import static com.oracle.svm.interpreter.metadata.Bytecodes.PUTFIELD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.PUTSTATIC; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.oracle.graal.pointsto.heap.ImageHeapConstant; +import com.oracle.svm.interpreter.metadata.BytecodeStream; +import com.oracle.svm.interpreter.metadata.Bytecodes; +import com.oracle.svm.interpreter.metadata.InterpreterConstantPool; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaField; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedObjectType; +import com.oracle.svm.interpreter.metadata.ReferenceConstant; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.graal.pointsto.constraints.UnsupportedFeatureException; +import com.oracle.graal.pointsto.util.AnalysisError; +import com.oracle.svm.core.meta.MethodPointer; +import com.oracle.svm.core.util.UserError; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.hosted.meta.HostedUniverse; + +import jdk.vm.ci.meta.ConstantPool; +import jdk.vm.ci.meta.ExceptionHandler; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaField; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.LocalVariableTable; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.UnresolvedJavaField; +import jdk.vm.ci.meta.UnresolvedJavaMethod; +import jdk.vm.ci.meta.UnresolvedJavaType; + +/** + * Allows to incrementally build constant pools from JVMCI provided bytecodes. This constant pool + * supports appending and de-duplicating constants, classes, fields, methods ... + *

+ * Once the constant pool is finished e.g. all methods in the class are processed for the + * interpreter, it must be transformed (via {@link #snapshot()}) into a runtime-ready + * {@link InterpreterConstantPool}. + */ +@Platforms(Platform.HOSTED_ONLY.class) +final class BuildTimeConstantPool { + + private static final ExceptionHandler[] EMPTY_EXCEPTION_HANDLERS = new ExceptionHandler[0]; + + private final InterpreterResolvedObjectType holder; + + private final Map constantCPI; + private final Map appendixCPI; + private final Map fieldCPI; + private final Map typeCPI; + private final Map methodCPI; + + final ArrayList entries; + + /** + * Creates runtime-ready constant pool for the interpreter. + */ + public InterpreterConstantPool snapshot() { + return InterpreterConstantPool.create(holder, entries.toArray()); + } + + BuildTimeConstantPool(InterpreterResolvedObjectType holder) { + this.holder = holder; + this.entries = new ArrayList<>(32); + this.entries.add(null); // index 0 always contains illegal entry + this.constantCPI = new HashMap<>(); + this.fieldCPI = new HashMap<>(); + this.typeCPI = new HashMap<>(); + this.methodCPI = new HashMap<>(); + this.appendixCPI = new HashMap<>(); + } + + public int length() { + return entries.size(); + } + + public int longConstant(long value) { + return constantCPI.computeIfAbsent(value, key -> { + JavaConstant javaConstant = BuildTimeInterpreterUniverse.singleton().primitiveConstant(value); + entries.add(javaConstant); + return entries.size() - 1; + }); + } + + public int intConstant(int value) { + return constantCPI.computeIfAbsent(value, key -> { + JavaConstant javaConstant = BuildTimeInterpreterUniverse.singleton().primitiveConstant(value); + entries.add(javaConstant); + return entries.size() - 1; + }); + } + + public int floatConstant(float value) { + return constantCPI.computeIfAbsent(value, key -> { + JavaConstant javaConstant = BuildTimeInterpreterUniverse.singleton().primitiveConstant(value); + entries.add(javaConstant); + return entries.size() - 1; + }); + } + + public int doubleConstant(double value) { + return constantCPI.computeIfAbsent(value, key -> { + JavaConstant javaConstant = BuildTimeInterpreterUniverse.singleton().primitiveConstant(value); + entries.add(javaConstant); + return entries.size() - 1; + }); + } + + public int stringConstant(String value) { + return constantCPI.computeIfAbsent(value, key -> { + JavaConstant javaConstant = BuildTimeInterpreterUniverse.singleton().stringConstant(value); + entries.add(javaConstant); + return entries.size() - 1; + }); + } + + public int typeConstant(JavaType type) { + if (!(type instanceof InterpreterResolvedJavaType || type instanceof UnresolvedJavaType)) { + throw new IllegalArgumentException("Type must be either InterpreterResolvedJavaType or UnresolvedJavaType"); + } + return typeCPI.computeIfAbsent(type, key -> { + entries.add(type); + return entries.size() - 1; + }); + } + + public int method(JavaMethod method) { + if (!(method instanceof InterpreterResolvedJavaMethod || method instanceof UnresolvedJavaMethod)) { + throw new IllegalArgumentException("Type must be either InterpreterResolvedJavaMethod or UnresolvedJavaMethod"); + } + return methodCPI.computeIfAbsent(method, (key) -> { + entries.add(method); + return entries.size() - 1; + }); + } + + public int field(JavaField field) { + if (!(field instanceof InterpreterResolvedJavaField || field instanceof UnresolvedJavaField)) { + throw new IllegalArgumentException("Type must be either InterpreterResolvedJavaField or UnresolvedJavaField"); + } + return fieldCPI.computeIfAbsent(field, (key) -> { + entries.add(field); + return entries.size() - 1; + }); + } + + private int appendixConstant(JavaConstant appendix) { + assert appendix instanceof ReferenceConstant || appendix.isNull(); + return appendixCPI.computeIfAbsent(appendix, key -> { + entries.add(appendix); + return entries.size() - 1; + }); + } + + public int weakObjectConstant(ImageHeapConstant imageHeapConstant) { + return constantCPI.computeIfAbsent(imageHeapConstant, key -> { + JavaConstant javaConstant = BuildTimeInterpreterUniverse.singleton().weakObjectConstant(imageHeapConstant); + entries.add(javaConstant); + return entries.size() - 1; + }); + } + + private int ldcConstant(Object javaConstantOrType) { + if (javaConstantOrType instanceof JavaConstant javaConstant) { + switch (javaConstant.getJavaKind()) { + case Boolean, Byte, Short, Char, Int: + return intConstant(javaConstant.asInt()); + case Float: + return floatConstant(javaConstant.asFloat()); + case Long: + return longConstant(javaConstant.asLong()); + case Double: + return doubleConstant(javaConstant.asDouble()); + case Object: + if (javaConstant instanceof ImageHeapConstant imageHeapConstant) { + return weakObjectConstant(imageHeapConstant); + } + } + } else if (javaConstantOrType instanceof JavaType javaType) { + JavaType interpreterType = BuildTimeInterpreterUniverse.singleton().typeOrUnresolved(javaType); + return typeConstant(interpreterType); + } + throw VMError.shouldNotReachHereUnexpectedInput(javaConstantOrType); + } + + public static BuildTimeConstantPool create(InterpreterResolvedObjectType type) { + BuildTimeConstantPool btcp = new BuildTimeConstantPool(type); + btcp.hydrate(type); + return btcp; + } + + private ExceptionHandler[] processExceptionHandlers(ExceptionHandler[] hostExceptionHandlers) { + if (hostExceptionHandlers.length == 0) { + return EMPTY_EXCEPTION_HANDLERS; + } + ExceptionHandler[] handlers = new ExceptionHandler[hostExceptionHandlers.length]; + for (int i = 0; i < handlers.length; i++) { + ExceptionHandler host = hostExceptionHandlers[i]; + JavaType resolvedCatchType = null; + JavaType interpreterCatchType = null; + int catchTypeCPI = 0; + if (!host.isCatchAll()) { + resolvedCatchType = host.getCatchType(); + interpreterCatchType = BuildTimeInterpreterUniverse.singleton().typeOrUnresolved(resolvedCatchType); + // catchTypeCPI must be patched. + catchTypeCPI = typeConstant(interpreterCatchType); + } + + handlers[i] = BuildTimeInterpreterUniverse.singleton() + .exceptionHandler(new ExceptionHandler(host.getStartBCI(), host.getEndBCI(), host.getHandlerBCI(), catchTypeCPI, interpreterCatchType)); + } + return handlers; + } + + public static boolean weedOut(InterpreterResolvedObjectType type, HostedUniverse hUniverse) { + boolean chasingFixpoint = false; + + methodsLoop: for (InterpreterResolvedJavaMethod method : BuildTimeInterpreterUniverse.singleton().allDeclaredMethods(type)) { + if (!method.needsMethodBody() || !method.isInterpreterExecutable()) { + method.setCode(null); + } + + byte[] code = method.getInterpretedCode(); + if (code == null || code.length == 0) { + continue; + } + + ResolvedJavaMethod originalMethod = method.getOriginalMethod(); + ConstantPool originalConstantPool = originalMethod.getConstantPool(); + for (int bci = 0; bci < BytecodeStream.endBCI(code); bci = BytecodeStream.nextBCI(code, bci)) { + int bytecode = BytecodeStream.currentBC(code, bci); + switch (bytecode) { + case INVOKEINTERFACE, INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC, INVOKEDYNAMIC -> { + int originalCPI; + if (bytecode == INVOKEDYNAMIC) { + originalCPI = BytecodeStream.readCPI4(code, bci); + } else { + originalCPI = BytecodeStream.readCPI(code, bci); + } + JavaMethod calleeOriginalJavaMethod = null; + try { + calleeOriginalJavaMethod = originalConstantPool.lookupMethod(originalCPI, bytecode); + } catch (UnsupportedFeatureException | UserError.UserException e) { + // ignore + } + if (calleeOriginalJavaMethod != null) { + JavaMethod calleeInterpreterMethod = BuildTimeInterpreterUniverse.singleton().methodOrUnresolved(calleeOriginalJavaMethod); + if (calleeInterpreterMethod instanceof InterpreterResolvedJavaMethod calleeInterpreterResolvedJavaMethod) { + if (!calleeInterpreterResolvedJavaMethod.isInterpreterExecutable()) { + HostedMethod calleeHostedMethod = hUniverse.optionalLookup(calleeOriginalJavaMethod); + if (calleeHostedMethod.isCompiled()) { + InterpreterUtil.log("[weedout] good. Call from %s @ bci=%s (interp) to %s (compiled) possible", method, bci, calleeInterpreterResolvedJavaMethod); + } else if (calleeHostedMethod.hasVTableIndex()) { + InterpreterUtil.log("[weedout] good. Virtual call from %s @ bci=%s (interp) to %s (compiled) possible", method, bci, calleeInterpreterResolvedJavaMethod); + } else if (calleeHostedMethod.getImplementations().length == 1) { + InterpreterUtil.log("[weedout] good. Virtual call from %s @ bci=%s (interp) has exactly one implementation available %s", method, bci, + calleeHostedMethod.getImplementations()[0]); + } else { + InterpreterUtil.log("[weedout] bad. %s downgraded to non-interpreter-executable.", method); + InterpreterUtil.log(" there is no way to call a compiled version of %s or to execute it in the interpreter, but it is considered reachable", + calleeInterpreterResolvedJavaMethod); + chasingFixpoint = true; + method.setCode(null); + continue methodsLoop; + } + } else { + /* the interpreter can dispatch the callee */ + } + } else { + /* + * not reached during analysis, let it fail during runtime if this + * call-site is reached + */ + } + } else { + InterpreterUtil.log("[weedout] ??? call from %s at bci=%s does not go anywhere", method, bci); + } + } + } + } + } + return chasingFixpoint; + } + + public void hydrate(InterpreterResolvedObjectType type) { + + List allDeclaredMethods = BuildTimeInterpreterUniverse.singleton().allDeclaredMethods(type); + + // LDC (single-byte CPI) bytecodes must be processed first. + processLDC(allDeclaredMethods); + + for (InterpreterResolvedJavaMethod method : allDeclaredMethods) { + ResolvedJavaMethod originalMethod = method.getOriginalMethod(); + method.setExceptionHandlers(processExceptionHandlers(originalMethod.getExceptionHandlers())); + + LocalVariableTable hostLocalVariableTable = method.getOriginalMethod().getLocalVariableTable(); + if (hostLocalVariableTable != null) { + method.setLocalVariableTable(BuildTimeInterpreterUniverse.processLocalVariableTable(hostLocalVariableTable)); + } + + if (!method.needsMethodBody()) { + VMError.guarantee(method.getInterpretedCode() == null); + } + + byte[] code = method.getInterpretedCode(); + if (code == null || code.length == 0) { + continue; + } + + InterpreterUtil.log("[hydrate] processing method=%s", method); + + ConstantPool originalConstantPool = originalMethod.getConstantPool(); + for (int bci = 0; bci < BytecodeStream.endBCI(code); bci = BytecodeStream.nextBCI(code, bci)) { + int bytecode = BytecodeStream.currentBC(code, bci); + switch (bytecode) { + case LDC: // fall-through + case LDC_W: // fall-through + case LDC2_W: { + int originalCPI = BytecodeStream.readCPI(code, bci); + int newCPI = 0; + // GR-44571: Somehow obtain an unresolved type to print useful error + // at runtime. + try { + Object originalConstant = originalConstantPool.lookupConstant(originalCPI); + newCPI = ldcConstant(originalConstant); + } catch (UnsupportedFeatureException | AnalysisError.TypeNotFoundError e) { + // cannot resolve type, ignore + } + BytecodeStream.patchCPI(code, bci, newCPI); + break; + } + case GETSTATIC: // fall-through + case PUTSTATIC: // fall-through + case GETFIELD: // fall-through + case PUTFIELD: { + int originalCPI = BytecodeStream.readCPI(code, bci); + int newCPI = 0; + JavaField originalJavaField = null; + try { + originalJavaField = originalConstantPool.lookupField(originalCPI, originalMethod, bytecode); + } catch (UnsupportedFeatureException e) { + // ignore + } + // GR-44571: Somehow obtain an unresolved field to print useful error + // at runtime. + if (originalJavaField != null) { + JavaField interpreterField = BuildTimeInterpreterUniverse.singleton().fieldOrUnresolved(originalJavaField); + newCPI = field(interpreterField); + } + BytecodeStream.patchCPI(code, bci, newCPI); + break; + } + case ANEWARRAY: // fall-through + case MULTIANEWARRAY: // fall-through + case NEW: // fall-through + case INSTANCEOF: // fall-through + case CHECKCAST: { + int originalCPI = BytecodeStream.readCPI(code, bci); + int newCPI = 0; + JavaType originalJavaType = null; + try { + originalJavaType = originalConstantPool.lookupType(originalCPI, bytecode); + } catch (UnsupportedFeatureException | AnalysisError.TypeNotFoundError e) { + // GR-44571: Type has not been seen during analysis (e.g. path + // has not been reached). + // Will patch the CPI with 0. + } + + // GR-44571: Somehow obtain an unresolved type to print useful error + // at runtime. + if (originalJavaType != null) { + JavaType interpreterType = BuildTimeInterpreterUniverse.singleton().typeOrUnresolved(originalJavaType); + newCPI = typeConstant(interpreterType); + } + BytecodeStream.patchCPI(code, bci, newCPI); + break; + } + case INVOKEINTERFACE: // fall-through + case INVOKEVIRTUAL: // fall-through + case INVOKESPECIAL: // fall-through + case INVOKESTATIC: + case INVOKEDYNAMIC: { + int originalCPI; + if (bytecode == INVOKEDYNAMIC) { + originalCPI = BytecodeStream.readCPI4(code, bci); + } else { + originalCPI = BytecodeStream.readCPI(code, bci); + } + JavaMethod originalJavaMethod = null; + int newCPI = 0; + try { + originalJavaMethod = originalConstantPool.lookupMethod(originalCPI, bytecode); + } catch (UnsupportedFeatureException | UserError.UserException e) { + // ignore + } + // GR-44571: Somehow obtain an unresolved method to print useful + // error at runtime. + if (originalJavaMethod != null) { + JavaMethod interpreterMethod = BuildTimeInterpreterUniverse.singleton().methodOrUnresolved(originalJavaMethod); + if (interpreterMethod instanceof InterpreterResolvedJavaMethod) { + ((InterpreterResolvedJavaMethod) interpreterMethod).setNativeEntryPoint(new MethodPointer((ResolvedJavaMethod) originalJavaMethod)); + InterpreterUtil.log("[hydrate] setting method pointer for %s", interpreterMethod); + } + newCPI = method(interpreterMethod); + } + + if (bytecode == INVOKEDYNAMIC) { + int newAppendixCPI = 0; + JavaConstant appendix = originalConstantPool.lookupAppendix(originalCPI, bytecode); + if (appendix != null) { + JavaConstant interpreterAppendix = BuildTimeInterpreterUniverse.singleton().appendix(appendix); + newAppendixCPI = appendixConstant(interpreterAppendix); + } else { + // The appendix may be null, in which case a NullConstant is stored + // in the CP. + newAppendixCPI = appendixConstant(JavaConstant.NULL_POINTER); + } + BytecodeStream.patchAppendixCPI(code, bci, newAppendixCPI); + } + + BytecodeStream.patchCPI(code, bci, newCPI); + break; + } + } + } + } + } + + private void processLDC(List allDeclaredMethods) { + for (InterpreterResolvedJavaMethod method : allDeclaredMethods) { + byte[] code = method.getInterpretedCode(); + if (code == null || code.length == 0) { + continue; + } + ConstantPool originalConstantPool = method.getOriginalMethod().getConstantPool(); + for (int bci = 0; bci < BytecodeStream.endBCI(code); bci = BytecodeStream.nextBCI(code, bci)) { + if (BytecodeStream.opcode(code, bci) == Bytecodes.LDC) { + try { + Object constant = originalConstantPool.lookupConstant(BytecodeStream.readCPI(code, bci)); + ldcConstant(constant); + } catch (UnsupportedFeatureException | AnalysisError.TypeNotFoundError e) { + // constant cannot be resolved, ignore + } + } + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/BuildTimeInterpreterUniverse.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/BuildTimeInterpreterUniverse.java new file mode 100644 index 000000000000..d4d233ba9e71 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/BuildTimeInterpreterUniverse.java @@ -0,0 +1,890 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEINTERFACE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESPECIAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEVIRTUAL; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.graal.pointsto.constraints.UnsupportedFeatureException; +import com.oracle.graal.pointsto.heap.ImageHeapConstant; +import com.oracle.graal.pointsto.infrastructure.OriginalClassProvider; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.svm.core.util.HostedStringDeduplication; +import com.oracle.svm.core.util.UserError; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.BytecodeStream; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaField; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedObjectType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedPrimitiveType; +import com.oracle.svm.interpreter.metadata.InterpreterUniverseImpl; +import com.oracle.svm.interpreter.metadata.InterpreterUnresolvedSignature; +import com.oracle.svm.interpreter.metadata.MetadataUtil; +import com.oracle.svm.interpreter.metadata.ReferenceConstant; +import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.hosted.meta.HostedUniverse; +import com.oracle.svm.hosted.pltgot.GOTEntryAllocator; +import com.oracle.svm.hosted.substitute.SubstitutionMethod; + +import jdk.graal.compiler.api.replacements.SnippetReflectionProvider; +import jdk.vm.ci.meta.ExceptionHandler; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaField; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.LineNumberTable; +import jdk.vm.ci.meta.Local; +import jdk.vm.ci.meta.LocalVariableTable; +import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.PrimitiveConstant; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.Signature; +import jdk.vm.ci.meta.UnresolvedJavaField; +import jdk.vm.ci.meta.UnresolvedJavaMethod; +import jdk.vm.ci.meta.UnresolvedJavaType; + +/** + * Maintains the view of classes, methods and fields needed to build the InterpreterUniverse, this + * is all metadata that is required for execution in the interpreter at run-time. + * + * The view is not a 1:1 mapping to other universes. For example, some methods might not be + * executable by the interpreter and are therefore stripped from the InterpreterUniverse partially + * (e.g. bytecode array is dropped, but a method pointer to the compiled version is still held). + * + * {@link #snapshot()} persist the current view into a InterpreterUniverse at the end of an image + * build and is then serialized to an additional file. + */ +@Platforms(Platform.HOSTED_ONLY.class) +public final class BuildTimeInterpreterUniverse { + + private static BuildTimeInterpreterUniverse INSTANCE; + + public static void freshSingletonInstance() { + INSTANCE = new BuildTimeInterpreterUniverse(); + } + + private final Map types; + private final Map unresolvedTypes; + private final Map unresolvedMethods; + private final Map unresolvedFields; + + private final Map fields; + + private final Map methods; + private final Map signatures; + private final Map primitiveConstants; + private final Map> strings; + private final Map> objectConstants; + + private final Map exceptionHandlers; + + private SnippetReflectionProvider snippetReflectionProvider; + + public SnippetReflectionProvider getSnippetReflectionProvider() { + return snippetReflectionProvider; + } + + private void setSnippetReflectionProvider(SnippetReflectionProvider snippetReflectionProvider) { + this.snippetReflectionProvider = snippetReflectionProvider; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static InterpreterResolvedObjectType createResolvedObjectType(ResolvedJavaType resolvedJavaType) { + BuildTimeInterpreterUniverse universe = BuildTimeInterpreterUniverse.singleton(); + String name = universe.dedup(resolvedJavaType.getName()); + Class clazz = OriginalClassProvider.getJavaClass(resolvedJavaType); + ResolvedJavaType originalType = MetadataUtil.requireNonNull(resolvedJavaType); + int modifiers = resolvedJavaType.getModifiers(); + InterpreterResolvedJavaType componentType; + if (originalType.isArray()) { + componentType = universe.type(originalType.getComponentType()); + } else { + componentType = null; + } + String sourceFileName = universe.dedup(resolvedJavaType.getSourceFileName()); + + ResolvedJavaType originalSuperclass = resolvedJavaType.getSuperclass(); + InterpreterResolvedObjectType superclass = null; + if (originalSuperclass != null) { + superclass = (InterpreterResolvedObjectType) universe.type(originalSuperclass); + } + + ResolvedJavaType[] originalInterfaces = resolvedJavaType.getInterfaces(); + InterpreterResolvedObjectType[] interfaces = new InterpreterResolvedObjectType[originalInterfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + interfaces[i] = (InterpreterResolvedObjectType) universe.type(originalInterfaces[i]); + } + + return InterpreterResolvedObjectType.createAtBuildTime(resolvedJavaType, name, modifiers, componentType, superclass, interfaces, null, clazz, sourceFileName); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static InterpreterResolvedJavaField createResolvedJavaField(ResolvedJavaField resolvedJavaField) { + ResolvedJavaField originalField = resolvedJavaField; + BuildTimeInterpreterUniverse universe = BuildTimeInterpreterUniverse.singleton(); + String name = universe.dedup(resolvedJavaField.getName()); + int modifiers = resolvedJavaField.getModifiers(); + JavaType fieldType = originalField.getType(); + + InterpreterResolvedJavaType type = universe.type((ResolvedJavaType) fieldType); + InterpreterResolvedObjectType declaringClass = universe.referenceType(originalField.getDeclaringClass()); + + return InterpreterResolvedJavaField.create(originalField, name, modifiers, type, declaringClass, 0, null); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static InterpreterResolvedJavaMethod createResolveJavaMethod(ResolvedJavaMethod originalMethod) { + assert originalMethod instanceof AnalysisMethod; + MetadataUtil.requireNonNull(originalMethod); + BuildTimeInterpreterUniverse universe = BuildTimeInterpreterUniverse.singleton(); + String name = universe.dedup(originalMethod.getName()); + int maxLocals = originalMethod.getMaxLocals(); + int maxStackSize = originalMethod.getMaxStackSize(); + int modifiers = originalMethod.getModifiers(); + InterpreterResolvedObjectType declaringClass = universe.referenceType(originalMethod.getDeclaringClass()); + InterpreterUnresolvedSignature signature = universe.unresolvedSignature(originalMethod.getSignature()); + byte[] interpretedCode = originalMethod.getCode() == null ? null : originalMethod.getCode().clone(); + + AnalysisMethod analysisMethod = (AnalysisMethod) originalMethod; + if (analysisMethod.wrapped instanceof SubstitutionMethod substitutionMethod) { + modifiers = substitutionMethod.getOriginal().getModifiers(); + if (substitutionMethod.hasBytecodes()) { + /* + * GR-53710: Keep bytecodes for substitutions, but only when there's no compiled + * entry. This is required to call Class.forName(String,boolean,ClassLoader) which + * is a substitution with no compiled entry. + */ + // Drop NATIVE flag from original method modifiers. + modifiers &= ~Modifier.NATIVE; + } + } + + LineNumberTable lineNumberTable = originalMethod.getLineNumberTable(); + return InterpreterResolvedJavaMethod.create( + originalMethod, + name, + maxLocals, + maxStackSize, + modifiers, + declaringClass, + signature, + interpretedCode, + null, + lineNumberTable, + null, + null, + InterpreterResolvedJavaMethod.VTBL_NO_ENTRY, + GOTEntryAllocator.GOT_NO_ENTRY, + InterpreterResolvedJavaMethod.EST_NO_ENTRY, + InterpreterResolvedJavaMethod.UNKNOWN_METHOD_ID); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static InterpreterUnresolvedSignature createUnresolvedSignature(Signature originalSignature) { + MetadataUtil.requireNonNull(originalSignature); + JavaType returnType = BuildTimeInterpreterUniverse.singleton().primitiveOrUnresolvedType(originalSignature.getReturnType(null)); + int parameterCount = originalSignature.getParameterCount(false); + JavaType[] parameterTypes = new JavaType[parameterCount]; + for (int i = 0; i < parameterCount; i++) { + parameterTypes[i] = BuildTimeInterpreterUniverse.singleton().primitiveOrUnresolvedType(originalSignature.getParameterType(i, null)); + } + return InterpreterUnresolvedSignature.create(originalSignature, returnType, parameterTypes); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static LocalVariableTable processLocalVariableTable(LocalVariableTable hostLocalVariableTable) { + Local[] hostLocals = hostLocalVariableTable.getLocals(); + if (hostLocals.length == 0) { + return InterpreterResolvedJavaMethod.EMPTY_LOCAL_VARIABLE_TABLE; + } + Local[] locals = new Local[hostLocals.length]; + for (int i = 0; i < locals.length; i++) { + Local host = hostLocals[i]; + JavaType hostType = host.getType(); + JavaType interpreterType = null; + if (hostType == null) { + InterpreterUtil.log("[processLocalVariableTable] BUG? host=%s. Remove local entry?", host); + } else { + interpreterType = BuildTimeInterpreterUniverse.singleton().typeOrUnresolved(hostType); + } + if (hostType instanceof AnalysisType && !((AnalysisType) hostType).isReachable()) { + /* + * Example: SecurityManager in java.lang.ThreadGroup.checkAccess. There is a + * graphbuilder plugin that makes sure getSecurityManager() always returns null, + * thus not reachable. + * + * For now, unresolved and unreachable types are not an issue since the + * LocalVariableTable attribute only keeps strictly unresolved types and primitives. + */ + } + locals[i] = BuildTimeInterpreterUniverse.singleton().local(new Local(host.getName(), interpreterType, host.getStartBCI(), host.getEndBCI(), host.getSlot())); + } + return BuildTimeInterpreterUniverse.singleton().localVariableTable(new LocalVariableTable(locals)); + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static void setNeedMethodBody(InterpreterResolvedJavaMethod thiz, boolean needMethodBody, MetaAccessProvider metaAccessProvider) { + if (thiz.needMethodBody) { + // skip, already scanned + return; + } else if (!needMethodBody) { + // nothing to do + return; + } + + if (thiz.getInterpretedCode() == null) { + // nothing to scan, method is not interpreterExecutable + return; + } + + thiz.needMethodBody = true; + + for (int bci = 0; bci < BytecodeStream.endBCI(thiz.getInterpretedCode()); bci = BytecodeStream.nextBCI(thiz.getInterpretedCode(), bci)) { + int opcode = BytecodeStream.opcode(thiz.getInterpretedCode(), bci); + switch (opcode) { + /* GR-53540: Handle invokedyanmic too */ + case INVOKESPECIAL, INVOKESTATIC, INVOKEVIRTUAL, INVOKEINTERFACE -> { + int originalCPI = BytecodeStream.readCPI(thiz.getInterpretedCode(), bci); + try { + JavaMethod method = thiz.getOriginalMethod().getConstantPool().lookupMethod(originalCPI, opcode); + if (!(method instanceof ResolvedJavaMethod resolvedJavaMethod)) { + continue; + } + if (!InterpreterFeature.callableByInterpreter(resolvedJavaMethod, metaAccessProvider)) { + InterpreterUtil.log("[process invokes] cannot execute %s due to call-site (%s) @ bci=%s is not callable by interpreter\n", thiz.getName(), bci, method); + thiz.setCode(null); + thiz.needMethodBody = false; + return; + } + + if (opcode == INVOKESPECIAL || opcode == INVOKESTATIC) { + continue; + } + + BuildTimeInterpreterUniverse.singleton().method(resolvedJavaMethod, false, metaAccessProvider); + } catch (UnsupportedFeatureException | UserError.UserException e) { + InterpreterUtil.log("[process invokes] lookup in method %s failed due to:", thiz.getOriginalMethod()); + InterpreterUtil.log(e); + // ignore, call will fail at run-time if reached + } + } + } + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static void setUnmaterializedConstantValue(InterpreterResolvedJavaField thiz, JavaConstant constant) { + assert constant == JavaConstant.NULL_POINTER || constant instanceof PrimitiveConstant || constant instanceof ImageHeapConstant; + BuildTimeInterpreterUniverse buildTimeInterpreterUniverse = BuildTimeInterpreterUniverse.singleton(); + switch (thiz.getJavaKind()) { + case Boolean, Byte, Short, Char, Int, Float, Long, Double: + assert constant instanceof PrimitiveConstant; + thiz.setUnmaterializedConstant(buildTimeInterpreterUniverse.constant(constant)); + break; + case Object: + if (constant.isNull()) { + // The value is always null. + thiz.setUnmaterializedConstant(buildTimeInterpreterUniverse.constant(JavaConstant.NULL_POINTER)); + } else if (constant.getJavaKind() == JavaKind.Illegal) { + // Materialized field without location e.g. DynamicHub#vtable. + thiz.setUnmaterializedConstant(buildTimeInterpreterUniverse.constant(JavaConstant.ILLEGAL)); + } else if (thiz.getType().isWordType()) { + // Can be a WordType with a primitive constant value. + thiz.setUnmaterializedConstant(buildTimeInterpreterUniverse.constant(constant)); + } else if (constant instanceof ImageHeapConstant imageHeapConstant) { + // Create a WeakImageHeapReference, the referent is only preserved iff it is + // present in the native image heap. + thiz.setUnmaterializedConstant(ReferenceConstant.createFromImageHeapConstant(imageHeapConstant)); + } else { + throw VMError.shouldNotReachHere("Unsupported unmaterialized constant value: " + constant); + } + break; + default: + throw VMError.shouldNotReachHere("Invalid field kind: " + thiz.getJavaKind()); + } + if (!thiz.isUndefined()) { + if (thiz.getType().isWordType()) { + VMError.guarantee(thiz.getUnmaterializedConstant().getJavaKind() == InterpreterToVM.wordJavaKind()); + } else { + VMError.guarantee(thiz.getUnmaterializedConstant().getJavaKind() == thiz.getJavaKind()); + } + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + private static final class LocalWrapper { + + final int hash; + final Local local; + + private LocalWrapper(Local local) { + this.local = MetadataUtil.requireNonNull(local); + this.hash = hashCode(this.local); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof LocalWrapper thatWrapper) { + Local that = thatWrapper.local; + return local.getName().equals(that.getName()) && local.getStartBCI() == that.getStartBCI() && local.getEndBCI() == that.getEndBCI() && local.getSlot() == that.getSlot() && + MetadataUtil.equals(local.getType(), that.getType()); + + } else { + return false; + } + } + + public static int hashCode(Local local) { + int h = MetadataUtil.hashCode(local.getName()); + h = h * 31 + local.getStartBCI(); + h = h * 31 + local.getEndBCI(); + h = h * 31 + local.getSlot(); + h = h * 31 + MetadataUtil.hashCode(local.getType()); + return h; + } + + @Override + public int hashCode() { + return hash; + } + } + + private final Map locals; + + @Platforms(Platform.HOSTED_ONLY.class) + private static final class LocalVariableTableWrapper { + final LocalVariableTable localVariableTable; + final Local[] locals; + final int hash; + + private LocalVariableTableWrapper(LocalVariableTable localVariableTable) { + this.localVariableTable = localVariableTable; + this.locals = localVariableTable.getLocals(); + int h = 0; + for (Local local : this.locals) { + h = h * 31 + LocalWrapper.hashCode(local); + } + this.hash = h; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof LocalVariableTableWrapper thatWrapper) { + if (localVariableTable == thatWrapper.localVariableTable) { + return true; + } + return Arrays.equals(locals, thatWrapper.locals); + } else { + return false; + } + } + + @Override + public int hashCode() { + return hash; + } + } + + private final Map localVariableTables; + + private final Map indyAppendices; + + public BuildTimeInterpreterUniverse() { + this.types = new HashMap<>(); + this.fields = new HashMap<>(); + this.methods = new HashMap<>(); + this.signatures = new HashMap<>(); + this.primitiveConstants = new HashMap<>(); + this.strings = new HashMap<>(); + this.objectConstants = new HashMap<>(); + this.exceptionHandlers = new HashMap<>(); + this.locals = new HashMap<>(); + this.localVariableTables = new HashMap<>(); + this.unresolvedTypes = new HashMap<>(); + this.unresolvedMethods = new HashMap<>(); + this.unresolvedFields = new HashMap<>(); + this.indyAppendices = new HashMap<>(); + } + + public static BuildTimeInterpreterUniverse singleton() { + return INSTANCE; + } + + public InterpreterResolvedObjectType referenceType(ResolvedJavaType resolvedJavaType) { + return (InterpreterResolvedObjectType) type(resolvedJavaType); + } + + @SuppressWarnings("static-method") + public String dedup(String string) { + return HostedStringDeduplication.singleton().deduplicate(string, false); + } + + public InterpreterResolvedPrimitiveType primitiveType(ResolvedJavaType resolvedJavaType) { + return (InterpreterResolvedPrimitiveType) type(resolvedJavaType); + } + + public InterpreterResolvedJavaType getType(ResolvedJavaType resolvedJavaType) { + return types.get(resolvedJavaType); + } + + public InterpreterResolvedJavaType type(ResolvedJavaType resolvedJavaType) { + assert resolvedJavaType instanceof AnalysisType; + InterpreterResolvedJavaType result = getType(resolvedJavaType); + if (result == null) { + synchronized (types) { + result = types.get(resolvedJavaType); + if (result == null) { + if (resolvedJavaType.isPrimitive()) { + result = InterpreterResolvedPrimitiveType.fromKind(JavaKind.fromPrimitiveOrVoidTypeChar(resolvedJavaType.getName().charAt(0))); + } else { + result = createResolvedObjectType(resolvedJavaType); + } + InterpreterUtil.log("[universe] Adding type '%s'", resolvedJavaType); + types.put(resolvedJavaType, result); + } + } + } + return result; + } + + public InterpreterResolvedJavaField field(ResolvedJavaField resolvedJavaField) { + assert resolvedJavaField instanceof AnalysisField; + InterpreterResolvedJavaField result = fields.get(resolvedJavaField); + if (result == null) { + synchronized (fields) { + result = fields.get(resolvedJavaField); + if (result == null) { + InterpreterUtil.log("[universe] Adding field '%s'", resolvedJavaField); + fields.put(resolvedJavaField, result = createResolvedJavaField(resolvedJavaField)); + } + } + } + return result; + } + + public InterpreterResolvedJavaMethod getMethod(ResolvedJavaMethod method) { + ResolvedJavaMethod wrapped = method; + if (wrapped instanceof HostedMethod hostedMethod) { + wrapped = hostedMethod.getWrapped(); + } + return methods.get(wrapped); + } + + public InterpreterResolvedJavaMethod method(ResolvedJavaMethod resolvedJavaMethod, boolean needsMethodBody, MetaAccessProvider metaAccessProvider) { + assert resolvedJavaMethod instanceof AnalysisMethod; + InterpreterResolvedJavaMethod result = getMethod(resolvedJavaMethod); + + if (result == null) { + synchronized (methods) { + result = methods.get(resolvedJavaMethod); + if (result == null) { + InterpreterUtil.log("[universe] Adding method '%s' with needsMethodBody=%s", resolvedJavaMethod, needsMethodBody); + methods.put(resolvedJavaMethod, result = createResolveJavaMethod(resolvedJavaMethod)); + } + } + } + + if (needsMethodBody) { + /* added explicitly, bytecodes are needed for interpretation */ + setNeedMethodBody(result, true, metaAccessProvider); + } + + return result; + } + + public JavaConstant weakObjectConstant(ImageHeapConstant imageHeapConstant) { + // Try to extract hosted references if possible. + // Some constants are stored in the interpreter metadata even if they are not included in + // the image heap e.g. String. + if (imageHeapConstant.isBackedByHostedObject()) { + Object value = snippetReflectionProvider.asObject(Object.class, imageHeapConstant.getHostedObject()); + if (value != null) { + return objectConstants.computeIfAbsent(imageHeapConstant, (key) -> ReferenceConstant.createFromNonNullReference(value)); + } + } + return objectConstants.computeIfAbsent(imageHeapConstant, (key) -> ReferenceConstant.createFromImageHeapConstant(imageHeapConstant)); + } + + public JavaConstant primitiveConstant(int value) { + return primitiveConstants.computeIfAbsent(value, (key) -> JavaConstant.forInt(value)); + } + + public JavaConstant primitiveConstant(long value) { + return primitiveConstants.computeIfAbsent(value, (key) -> JavaConstant.forLong(value)); + } + + public JavaConstant primitiveConstant(float value) { + return primitiveConstants.computeIfAbsent(value, (key) -> JavaConstant.forFloat(value)); + } + + public JavaConstant primitiveConstant(double value) { + return primitiveConstants.computeIfAbsent(value, (key) -> JavaConstant.forDouble(value)); + } + + public JavaConstant stringConstant(String value) { + return strings.computeIfAbsent(value, (key) -> ReferenceConstant.createFromNonNullReference(Objects.requireNonNull(value))); + } + + public JavaType primitiveOrUnresolvedType(JavaType type) { + // Primitives are always resolved. + if (type.getJavaKind().isPrimitive()) { + return InterpreterResolvedPrimitiveType.fromKind(type.getJavaKind()); + } + return unresolvedTypes.computeIfAbsent(dedup(MetadataUtil.toUniqueString(type)), UnresolvedJavaType::create); + } + + public JavaConstant appendix(JavaConstant appendix) { + Objects.requireNonNull(appendix); + JavaConstant result = indyAppendices.get(appendix); + if (result == null) { + synchronized (indyAppendices) { + result = indyAppendices.get(appendix); + if (result == null) { + if (appendix instanceof ImageHeapConstant imageHeapConstant) { + result = weakObjectConstant(imageHeapConstant); + } else { + VMError.shouldNotReachHere("unexpected appendix: " + appendix); + } + indyAppendices.put(appendix, result); + } + } + } + return result; + } + + public InterpreterUnresolvedSignature unresolvedSignature(Signature signature) { + return signatures.computeIfAbsent(MetadataUtil.toUniqueString(signature), key -> createUnresolvedSignature(signature)); + } + + public JavaType typeOrUnresolved(JavaType type) { + JavaType result = types.get(type); + if (result == null) { + // UnresolvedJavaType can be trusted because it only refers to type by name. + result = primitiveOrUnresolvedType(type); + } + return result; + } + + public ExceptionHandler exceptionHandler(ExceptionHandler handler) { + return exceptionHandlers.computeIfAbsent(handler, Function.identity()); + } + + public Local local(Local local) { + return locals.computeIfAbsent(new LocalWrapper(local), Function.identity()).local; + } + + public LocalVariableTable localVariableTable(LocalVariableTable localVariableTable) { + return localVariableTables.computeIfAbsent(new LocalVariableTableWrapper(localVariableTable), Function.identity()).localVariableTable; + } + + // returns InterpreterResolvedJavaField | UnresolvedJavaField + public JavaField fieldOrUnresolved(JavaField field) { + JavaField result = fields.get(field); + if (result == null) { + // Do not trust incoming UnresolvedJavaField, an unresolved field may have a resolved + // declaring type. + JavaType holder = primitiveOrUnresolvedType(field.getDeclaringClass()); + JavaType type = primitiveOrUnresolvedType(field.getType()); + result = unresolvedFields.computeIfAbsent(MetadataUtil.toUniqueString(field), key -> new UnresolvedJavaField(holder, dedup(field.getName()), type)); + } + return result; + } + + // returns InterpreterResolvedJavaMethod | UnresolvedJavaMethod + public JavaMethod methodOrUnresolved(JavaMethod method0) { + final JavaMethod method = method0 instanceof HostedMethod hostedMethod ? hostedMethod.wrapped : method0; + JavaMethod result = methods.get(method); + if (result == null) { + // Do not trust incoming unresolved method, it may have resolved holder. + JavaType holder = primitiveOrUnresolvedType(method.getDeclaringClass()); + Signature signature = unresolvedSignature(method.getSignature()); + result = unresolvedMethods.computeIfAbsent(MetadataUtil.toUniqueString(method), key -> new UnresolvedJavaMethod(dedup(method.getName()), signature, holder)); + } + return result; + } + + private Map> classToMethods; + private Map> classToFields; + + /** + * This classes cause problems when resolving constant pool entries. + */ + public void createConstantPools(HostedUniverse hUniverse) { + setSnippetReflectionProvider(hUniverse.getSnippetReflection()); + + this.classToMethods = methods.values().stream().collect(Collectors.groupingBy(InterpreterResolvedJavaMethod::getDeclaringClass)); + this.classToFields = fields.values().stream().collect(Collectors.groupingBy(InterpreterResolvedJavaField::getDeclaringClass)); + + boolean needsAnotherRound = true; + int iterations = 0; + while (needsAnotherRound) { + needsAnotherRound = false; + iterations++; + InterpreterUtil.log("[weedout] iteration %s", iterations); + for (InterpreterResolvedJavaType type : types.values()) { + if (type instanceof InterpreterResolvedObjectType referenceType) { + needsAnotherRound |= BuildTimeConstantPool.weedOut(referenceType, hUniverse); + } + } + } + + for (InterpreterResolvedJavaType type : types.values()) { + if (type instanceof InterpreterResolvedObjectType referenceType) { + BuildTimeConstantPool buildTimeConstantPool = BuildTimeConstantPool.create(referenceType); + referenceType.setConstantPool(buildTimeConstantPool.snapshot()); + } + } + } + + public List allDeclaredMethods(InterpreterResolvedObjectType type) { + return classToMethods.getOrDefault(type, Collections.emptyList()); + } + + public List allDeclaredFields(InterpreterResolvedObjectType type) { + return classToFields.getOrDefault(type, Collections.emptyList()); + } + + public Collection getFields() { + return fields.values(); + } + + public Collection getMethods() { + return methods.values(); + } + + private static boolean isReachable(InterpreterResolvedJavaType type) { + if (type instanceof InterpreterResolvedPrimitiveType) { + return true; + } + AnalysisType originalType = (AnalysisType) ((InterpreterResolvedObjectType) type).getOriginalType(); + return originalType.isReachable(); + } + + static boolean isReachable(InterpreterResolvedJavaField field) { + AnalysisField originalField = (AnalysisField) field.getOriginalField(); + // Artificial reachability ensures that the interpreter keeps the field metadata around, + // but reachability still depends on the reachability of the declaring class and field type. + return field.isArtificiallyReachable() || (originalField.isReachable() && originalField.getDeclaringClass().isReachable()); + } + + static boolean isReachable(InterpreterResolvedJavaMethod method) { + AnalysisMethod originalMethod = (AnalysisMethod) method.getOriginalMethod(); + return originalMethod.isReachable() && originalMethod.getDeclaringClass().isReachable(); + } + + public void purgeUnreachable(MetaAccessProvider metaAccessProvider) { + + List nonReachableTypes = new ArrayList<>(); + for (InterpreterResolvedJavaType type : types.values()) { + if (!isReachable(type)) { + nonReachableTypes.add((InterpreterResolvedObjectType) type); + } + } + Iterator> iteratorMethods = methods.entrySet().iterator(); + while (iteratorMethods.hasNext()) { + Map.Entry next = iteratorMethods.next(); + InterpreterResolvedJavaMethod interpreterMethod = next.getValue(); + InterpreterResolvedObjectType declaringClass = interpreterMethod.getDeclaringClass(); + if (isReachable(interpreterMethod) && interpreterMethod.isInterpreterExecutable() && !isReachable(declaringClass)) { + InterpreterUtil.log("[purge] declaring class=%s of method=%s is not reachable, which cannot be represented in the interpreter universe currently", declaringClass, interpreterMethod); + VMError.shouldNotReachHere("declaring class should be reachable"); + } + + int removeMethodReason = 0; + + if (!isReachable(interpreterMethod) || !interpreterMethod.isInterpreterExecutable()) { + /* + * we might need that method as a holder for the call-site signature and vtable + * index + */ + InterpreterUtil.log("[purge] downgrading method=%s", interpreterMethod); + setNeedMethodBody(interpreterMethod, false, metaAccessProvider); + if (!isReachable(declaringClass)) { + InterpreterUtil.log("[purge] remove declaring class=%s", declaringClass); + nonReachableTypes.add(declaringClass); + removeMethodReason |= (1 << 0); + } + } + + if (!isReachable(interpreterMethod)) { + AnalysisMethod analysisMethod = (AnalysisMethod) interpreterMethod.getOriginalMethod(); + boolean isRoot = analysisMethod.isDirectRootMethod() || analysisMethod.isVirtualRootMethod() || analysisMethod.isInvoked(); + int implementations = analysisMethod.collectMethodImplementations(true).size(); + if (!isRoot && (next.getValue().isStatic() || implementations <= 1)) { + removeMethodReason |= (1 << 1); + } + } + if (removeMethodReason > 0) { + InterpreterUtil.log("[purge] remove method '%s' with reason %s", interpreterMethod, Integer.toBinaryString(removeMethodReason)); + iteratorMethods.remove(); + } + } + Iterator> iteratorFields = fields.entrySet().iterator(); + while (iteratorFields.hasNext()) { + Map.Entry next = iteratorFields.next(); + if (!isReachable(next.getValue()) || !isReachable(next.getValue().getDeclaringClass()) || !isReachable(next.getValue().getType())) { + InterpreterUtil.log("[purge] remove field '%s'", next.getValue()); + iteratorFields.remove(); + } + } + for (InterpreterResolvedObjectType nonReachableType : nonReachableTypes) { + InterpreterUtil.log("[purge] remove type '%s'", nonReachableType); + types.remove(nonReachableType.getOriginalType()); + } + + // Verification. + for (InterpreterResolvedJavaType type : types.values()) { + VMError.guarantee(isReachable(type)); + } + + for (InterpreterResolvedJavaField field : fields.values()) { + VMError.guarantee(isReachable(field)); + } + + for (InterpreterResolvedJavaMethod method : methods.values()) { + VMError.guarantee(isReachable(method) || !method.isStatic(), "non reachable"); + } + } + + static void topSort(ResolvedJavaType type, List order, Set seen) { + if (type == null || seen.contains(type)) { + return; + } + topSort(type.getSuperclass(), order, seen); + topSort(type.getComponentType(), order, seen); + for (ResolvedJavaType interf : type.getInterfaces()) { + topSort(interf, order, seen); + } + order.add(type); + seen.add(type); + } + + static List topologicalOrder(Collection types) { + List order = new ArrayList<>(types.size()); + Set seen = Collections.newSetFromMap(new IdentityHashMap<>(types.size())); + for (ResolvedJavaType type : types) { + topSort(type, order, seen); + } + assert types.size() == order.size(); + return order; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public InterpreterUniverseImpl snapshot() { + Collection values = types.values(); + // All type dependencies must appear strictly before in the ordering for serialization. + // Superclasses, super interfaces and component type of T must appear strictly before T. + List topologicalOrder = topologicalOrder(values); + assert checkOrder(topologicalOrder); + assert topologicalOrder.stream().allMatch(type -> type instanceof InterpreterResolvedJavaType); + List result = (List) topologicalOrder; + return new InterpreterUniverseImpl(result, fields.values(), methods.values()); + } + + private static List dependencies(ResolvedJavaType type) { + List result = new ArrayList<>(Arrays.asList(type.getInterfaces())); + if (type.getSuperclass() != null) { + result.add(type.getSuperclass()); + } + if (type.getComponentType() != null) { + result.add(type.getComponentType()); + } + return result; + } + + private static boolean checkOrder(List order) { + Set seen = Collections.newSetFromMap(new IdentityHashMap<>(order.size())); + for (ResolvedJavaType type : order) { + for (ResolvedJavaType strictlyBefore : dependencies(type)) { + if (!seen.contains(strictlyBefore)) { + return false; + } + } + seen.add(type); + } + return true; + } + + /** + * Converts a {@link JavaConstant} into constant supported by the interpreter. + * + * @param constant a constant handled from analysis or hosted world. + * @return a {@link PrimitiveConstant} or a {@link JavaConstant#NULL_POINTER}. + */ + public JavaConstant constant(JavaConstant constant) { + if (constant.getClass() == PrimitiveConstant.class) { + PrimitiveConstant primitiveConstant = (PrimitiveConstant) constant; + // Dedup constants. + switch (primitiveConstant.getJavaKind()) { + case Int: + return primitiveConstant(primitiveConstant.asInt()); + case Float: + return primitiveConstant(primitiveConstant.asFloat()); + case Long: + return primitiveConstant(primitiveConstant.asLong()); + case Double: + return primitiveConstant(primitiveConstant.asDouble()); + default: + return constant; + } + } + if (constant.isNull()) { + return JavaConstant.NULL_POINTER; + } + throw VMError.shouldNotReachHere("unsupported constant: " + constant); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/ConstantBytecodes.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/ConstantBytecodes.java new file mode 100644 index 000000000000..682244fcba82 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/ConstantBytecodes.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import jdk.graal.compiler.api.replacements.Fold; + +import com.oracle.svm.interpreter.metadata.Bytecodes; +import com.oracle.svm.core.util.VMError; + +final class ConstantBytecodes { + + private ConstantBytecodes() { + throw VMError.shouldNotReachHereAtRuntime(); + } + + /** + * Version of {@link Bytecodes#lengthOf(int)} that returns a constant. The opcode must be a + * compile-time constant. + * + * @see Bytecodes#lengthOf(int) + */ + @Fold + public static int lengthOf(int opcode) { + return Bytecodes.lengthOf(opcode); + } + + /** + * Version of {@link Bytecodes#stackEffectOf(int)} that returns a constant. The opcode must be a + * compile-time constant. + * + * @see Bytecodes#stackEffectOf(int) + */ + @Fold + public static int stackEffectOf(int opcode) { + return Bytecodes.stackEffectOf(opcode); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/CremaFeature.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/CremaFeature.java new file mode 100644 index 000000000000..c0e8eaeabbde --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/CremaFeature.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.hub.RuntimeClassLoading; + +import com.oracle.svm.hosted.FeatureImpl; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.hosted.Feature; + +import java.util.Arrays; +import java.util.List; + +/** + * In this mode the interpreter is used to execute previously (= image build-time) unknown methods, + * i.e. methods that are loaded or created at run-time. + */ + +@Platforms(Platform.HOSTED_ONLY.class) +@AutomaticallyRegisteredFeature +public class CremaFeature implements InternalFeature { + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return RuntimeClassLoading.isSupported(); + } + + @Override + public List> getRequiredFeatures() { + return Arrays.asList(InterpreterFeature.class); + } + + @Override + public void afterAbstractImageCreation(AfterAbstractImageCreationAccess access) { + FeatureImpl.AfterAbstractImageCreationAccessImpl accessImpl = ((FeatureImpl.AfterAbstractImageCreationAccessImpl) access); + + /* create vtable enter stubs */ + int maxVtableIndex = 0x100; + InterpreterStubSection stubSection = ImageSingletons.lookup(InterpreterStubSection.class); + stubSection.createInterpreterVtableEnterStubSection(accessImpl.getImage(), maxVtableIndex); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/DebuggerFeature.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/DebuggerFeature.java new file mode 100644 index 000000000000..201209bbfd2a --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/DebuggerFeature.java @@ -0,0 +1,757 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEDYNAMIC; +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.EST_NO_ENTRY; +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.VTBL_NO_ENTRY; +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.VTBL_ONE_IMPL; +import static com.oracle.svm.interpreter.metadata.InterpreterUniverseImpl.toHexString; +import static com.oracle.svm.hosted.pltgot.GOTEntryAllocator.GOT_NO_ENTRY; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.graalvm.collections.EconomicMap; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.word.Pointer; + +import com.oracle.graal.pointsto.heap.ImageHeapConstant; +import com.oracle.graal.pointsto.infrastructure.OriginalClassProvider; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.svm.core.BuildArtifacts; +import com.oracle.svm.core.FunctionPointerHolder; +import com.oracle.svm.core.InvalidMethodPointerHandler; +import com.oracle.svm.core.ParsingReason; +import com.oracle.svm.core.RuntimeAssertionsSupport; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.meta.MethodPointer; +import com.oracle.svm.core.option.HostedOptionValues; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.classfile.ClassFile; +import com.oracle.svm.interpreter.metadata.BytecodeStream; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaField; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedObjectType; +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; +import com.oracle.svm.interpreter.metadata.InterpreterUniverseImpl; +import com.oracle.svm.interpreter.metadata.MetadataUtil; +import com.oracle.svm.interpreter.metadata.ReferenceConstant; +import com.oracle.svm.interpreter.metadata.serialization.SerializationContext; +import com.oracle.svm.interpreter.metadata.serialization.Serializers; +import com.oracle.svm.hosted.FeatureImpl; +import com.oracle.svm.hosted.NativeImageGenerator; +import com.oracle.svm.hosted.code.CompileQueue; +import com.oracle.svm.hosted.code.SubstrateCompilationDirectives; +import com.oracle.svm.hosted.image.NativeImageHeap; +import com.oracle.svm.hosted.meta.HostedField; +import com.oracle.svm.hosted.meta.HostedMetaAccess; +import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.hosted.meta.HostedType; +import com.oracle.svm.hosted.meta.HostedUniverse; +import com.oracle.svm.hosted.pltgot.GOTEntryAllocator; +import com.oracle.svm.hosted.pltgot.HostedPLTGOTConfiguration; +import com.oracle.svm.hosted.pltgot.IdentityMethodAddressResolverFeature; +import com.oracle.svm.hosted.pltgot.PLTGOTOptions; +import com.oracle.svm.hosted.snippets.SubstrateGraphBuilderPlugins; +import com.oracle.svm.hosted.substitute.SubstitutionMethod; +import com.oracle.svm.util.ModuleSupport; + +import jdk.graal.compiler.api.replacements.SnippetReflectionProvider; +import jdk.graal.compiler.core.common.SuppressFBWarnings; +import jdk.graal.compiler.nodes.ValueNode; +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration; +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderContext; +import jdk.graal.compiler.nodes.graphbuilderconf.InvocationPlugin; +import jdk.graal.compiler.nodes.graphbuilderconf.InvocationPlugins; +import jdk.graal.compiler.options.OptionValues; +import jdk.graal.compiler.phases.util.Providers; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.PrimitiveConstant; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.UnresolvedJavaMethod; + +/** + * Also known as "YellowBird". + * + * In this mode the interpreter is used as an alternative execution engine to already AOT compiled + * methods in an image. This is needed to enable bytecode level debugging for JDWP. + * + * This also implies that all methods that are AOT compiled, need their bytecodes collected at image + * build-time. + */ +@Platforms(Platform.HOSTED_ONLY.class) +@AutomaticallyRegisteredFeature +public class DebuggerFeature implements InternalFeature { + private Method enterInterpreterMethod; + private InterpreterStubTable enterStubTable = null; + private final List> classesUsedByInterpreter = new ArrayList<>(); + private Set methodsProcessedDuringAnalysis; + private InvocationPlugins invocationPlugins; + private static final String SYNTHETIC_ASSERTIONS_DISABLED_FIELD_NAME = "$assertionsDisabled"; + + public DebuggerFeature() { + if (ModuleSupport.modulePathBuild) { + /* SVM_JDWP_RESIDENT */ + ModuleSupport.accessPackagesToClass(ModuleSupport.Access.EXPORT, InterpreterFeature.class, false, + "jdk.internal.vm.ci", "jdk.vm.ci.code", "jdk.vm.ci.meta"); + ModuleSupport.accessPackagesToClass(ModuleSupport.Access.EXPORT, InterpreterFeature.class, false, + "java.base", "jdk.internal.misc"); + + ModuleSupport.accessPackagesToClass(ModuleSupport.Access.EXPORT, InterpreterFeature.class, false, + "org.graalvm.nativeimage", "org.graalvm.nativeimage.impl"); + + ModuleSupport.accessPackagesToClass(ModuleSupport.Access.EXPORT, InterpreterFeature.class, false, + "org.graalvm.nativeimage.base"); + + /* SVM_JDWP_COMMON */ + ModuleSupport.accessPackagesToClass(ModuleSupport.Access.EXPORT, InterpreterUniverse.class, false, + "jdk.internal.vm.ci", "jdk.vm.ci.meta"); + } + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return InterpreterOptions.DebuggerWithInterpreter.getValue(); + } + + @Override + public List> getRequiredFeatures() { + return Arrays.asList( + InterpreterFeature.class, + IdentityMethodAddressResolverFeature.class); + } + + private static Class getArgumentClass(GraphBuilderContext b, ResolvedJavaMethod targetMethod, int parameterIndex, ValueNode arg) { + SubstrateGraphBuilderPlugins.checkParameterUsage(arg.isConstant(), b, targetMethod, parameterIndex, "parameter is not a compile time constant"); + return OriginalClassProvider.getJavaClass(b.getConstantReflection().asJavaType(arg.asJavaConstant())); + } + + @Override + public void registerInvocationPlugins(Providers providers, GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + invocationPlugins = plugins.getInvocationPlugins(); + InvocationPlugins.Registration r = new InvocationPlugins.Registration(invocationPlugins, InterpreterDirectives.class); + + r.register(new InvocationPlugin.RequiredInvocationPlugin("markKlass", Class.class) { + @Override + public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode arg1) { + Class targetKlass = getArgumentClass(b, targetMethod, 1, arg1); + InterpreterUtil.log("[invocation plugin] Adding %s", targetKlass); + classesUsedByInterpreter.add(targetKlass); + + /* no-op in compiled code */ + return true; + } + }); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + enterStubTable = new InterpreterStubTable(); + + VMError.guarantee(PLTGOTOptions.EnablePLTGOT.getValue()); + } + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + FeatureImpl.BeforeAnalysisAccessImpl accessImpl = (FeatureImpl.BeforeAnalysisAccessImpl) access; + + try { + enterInterpreterMethod = InterpreterStubSection.class.getMethod("enterInterpreterStub", int.class, Pointer.class); + accessImpl.registerAsRoot(enterInterpreterMethod, false, "stub for interpreter"); + + // Holds references that must be kept alive in the image heap. + access.registerAsAccessed(DebuggerSupport.class.getDeclaredField("referencesInImage")); + access.registerAsAccessed(DebuggerSupport.class.getDeclaredField("methodPointersInImage")); + + accessImpl.registerAsRoot(System.class.getDeclaredMethod("arraycopy", Object.class, int.class, Object.class, int.class, int.class), true, + "Allow interpreting methods that call System.arraycopy"); + } catch (NoSuchMethodException | NoSuchFieldException e) { + throw VMError.shouldNotReachHereAtRuntime(); + } + + registerStringConcatenation(accessImpl); + + // GR-53734: Known issues around reachability + try { + // JDK code introduced a new optional intrinsic: + // https://github.com/openjdk/jdk22u/commit/a4e9168bab1c2872ce2dbc7971a45c259270271f + // consider DualPivotQuicksort.java:268, int.class is not needed if the sort helper + // is inlined, therefore it's not needed. Still needed for interpreter execution. + access.registerAsAccessed(Integer.class.getField("TYPE")); + } catch (NoSuchFieldException e) { + throw VMError.shouldNotReachHereAtRuntime(); + } + + methodsProcessedDuringAnalysis = new HashSet<>(); + + ImageSingletons.add(DebuggerSupport.class, new DebuggerSupport()); + } + + private static void registerStringConcatenation(FeatureImpl.BeforeAnalysisAccessImpl accessImpl) { + /* + * String concatenation a0 + a1 + ... + an is compiled by the Eclipse Java Compiler (ecj) + * to: + * + * new StringBuilder(a0).append(a1).append(a2) ... append(an).toString() + * + * javac emits INVOKEDYNAMIC-based String concatenation instead. + * + * String concatenation is heavily optimized by the compiler to the point that most + * StringBuilder methods/constructor are not included if they are not explicitly used + * outside String concatenation. + * + * These registrations enable the interpreter to "interpret" StringBuilder-based String + * concatenation optimized away by the compiler. + */ + try { + List appendMethods = Arrays.stream(StringBuilder.class.getDeclaredMethods()) + .filter(m -> "append".equals(m.getName())) + .collect(Collectors.toList()); + for (Method m : appendMethods) { + accessImpl.registerAsRoot(m, false, "string concat in interpreter"); + } + for (Constructor c : StringBuilder.class.getDeclaredConstructors()) { + accessImpl.registerAsRoot(c, true, "string concat in interpreter"); + } + accessImpl.registerAsRoot(StringBuilder.class.getConstructor(), true, "string concat in interpreter"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + private static boolean isReachable(AnalysisMethod m) { + return m.isReachable() || m.isDirectRootMethod() || m.isVirtualRootMethod(); + } + + @Override + public void duringAnalysis(DuringAnalysisAccess access) { + FeatureImpl.DuringAnalysisAccessImpl accessImpl = (FeatureImpl.DuringAnalysisAccessImpl) access; + + boolean addedIndyHelper = false; + for (AnalysisMethod m : accessImpl.getUniverse().getMethods()) { + if (isReachable(m)) { + continue; + } + if (m.getName().startsWith("invoke") && m.getDeclaringClass().getName().equals("Ljava/lang/invoke/MethodHandle;")) { + accessImpl.registerAsRoot(m, true, "method handle for interpreter"); + SubstrateCompilationDirectives.singleton().registerForcedCompilation(m); + InterpreterUtil.log("[during analysis] Force entry point for %s and mark as reachable", m); + addedIndyHelper = true; + } + } + if (addedIndyHelper) { + access.requireAnalysisIteration(); + return; + } + + if (!classesUsedByInterpreter.isEmpty()) { + access.requireAnalysisIteration(); + for (Class k : classesUsedByInterpreter) { + accessImpl.registerAsUsed(k); + Arrays.stream(k.getDeclaredMethods()).filter(m -> m.getName().startsWith("test")).forEach(m -> { + AnalysisMethod aMethod = accessImpl.getMetaAccess().lookupJavaMethod(m); + VMError.guarantee(!aMethod.isConstructor()); + accessImpl.registerAsRoot(aMethod, aMethod.isConstructor(), "reached due to interpreter directive"); + InterpreterUtil.log("[during analysis] Adding method %s", m); + }); + } + classesUsedByInterpreter.clear(); + return; + } + + DebuggerSupport supportImpl = DebuggerSupport.singleton(); + SnippetReflectionProvider snippetReflection = accessImpl.getUniverse().getSnippetReflection(); + + for (AnalysisMethod method : accessImpl.getUniverse().getMethods()) { + /* + * Hack: Add frame info for every reachable method, so we have local infos on compiled + * frames. A proper solution is to externalize this information in our metadata file and + * retrieve it at run-time on demand. + */ + SubstrateCompilationDirectives.singleton().registerFrameInformationRequired(method); + + if (method.isReachable() && !methodsProcessedDuringAnalysis.contains(method)) { + byte[] code = method.getCode(); + if (code == null) { + continue; + } + AnalysisType declaringClass = method.getDeclaringClass(); + if (!declaringClass.isReachable() && !declaringClass.getName().toLowerCase(Locale.ROOT).contains("hosted")) { + /* + * It rarely happens that a method is reachable but its declaring class is not. + * This can't be represented in the interpreter universe currently, thus we + * force the declaring class to be reached. + * + * Example: ImageSingletons.lookup(Ljava/lang/Class); + */ + InterpreterUtil.log("[during analysis] declaring class %s of method %s is not reachable, force it as root", declaringClass, method); + accessImpl.registerAsUsed(declaringClass, "interpreter needs dynamic hub at runtime for this class"); + access.requireAnalysisIteration(); + } + for (int bci = 0; bci < BytecodeStream.endBCI(code); bci = BytecodeStream.nextBCI(code, bci)) { + int bytecode = BytecodeStream.currentBC(code, bci); + if (bytecode == INVOKEDYNAMIC) { + int targetMethodCPI = BytecodeStream.readCPI4(code, bci); + JavaMethod targetMethod = method.getConstantPool().lookupMethod(targetMethodCPI, bytecode); + /* + * SVM optimizes away javac's INVOKDYNAMIC-based String concatenation e.g. + * MH.makeConcatWithConstants(...) . The CP method entry remains unresolved. + * + * Only reachable call sites should have its method and appendix included in + * the image, for now, ALL INVOKEDYNAMIC call sites of reachable methods are + * included. + */ + if (targetMethod instanceof UnresolvedJavaMethod) { + method.getConstantPool().loadReferencedType(targetMethodCPI, bytecode); + targetMethod = method.getConstantPool().lookupMethod(targetMethodCPI, bytecode); + } + if (targetMethod instanceof AnalysisMethod analysisMethod) { + accessImpl.registerAsRoot(analysisMethod, true, "forced for indy support in interpreter"); + InterpreterUtil.log("[during analysis] force %s mark as reachable", targetMethod); + } + + JavaConstant appendixConstant = method.getConstantPool().lookupAppendix(targetMethodCPI, bytecode); + if (appendixConstant instanceof ImageHeapConstant imageHeapConstant) { + supportImpl.ensureConstantIsInImageHeap(snippetReflection, imageHeapConstant); + } + } + } + methodsProcessedDuringAnalysis.add(method); + } + } + supportImpl.trimForcedReferencesInImageHeap(); + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + VMError.guarantee(InterpreterToVM.wordJavaKind() == JavaKind.Long || + InterpreterToVM.wordJavaKind() == JavaKind.Int); + } + + @Override + public void beforeCompilation(BeforeCompilationAccess access) { + FeatureImpl.BeforeCompilationAccessImpl accessImpl = (FeatureImpl.BeforeCompilationAccessImpl) access; + HostedUniverse hUniverse = accessImpl.getUniverse(); + HostedMetaAccess hMetaAccess = accessImpl.getMetaAccess(); + MetaAccessProvider aMetaAccess = hMetaAccess.getWrapped(); + BuildTimeInterpreterUniverse iUniverse = BuildTimeInterpreterUniverse.singleton(); + + for (HostedType hType : hUniverse.getTypes()) { + AnalysisType aType = hType.getWrapped(); + if (aType.isReachable()) { + iUniverse.type(aType); + for (ResolvedJavaField staticField : aType.getStaticFields()) { + if (staticField instanceof AnalysisField analysisStaticField && !analysisStaticField.isWritten()) { + /* + * Assertions are implemented by generating a boolean $assertionsDisabled + * static field, but native-image substitutes the field reads by a constant, + * making the field unreachable sometimes. The interpreter must artificially + * preserve the metadata without making it reachable to the analysis. In + * some cases, $assertionsDisabled is written in not-yet-executed static + * initializers, it can't be made read-only always. + */ + if (staticField.isStatic() && staticField.isSynthetic() && staticField.getName().startsWith(SYNTHETIC_ASSERTIONS_DISABLED_FIELD_NAME)) { + Class declaringClass = aType.getJavaClass(); + boolean value = !RuntimeAssertionsSupport.singleton().desiredAssertionStatus(declaringClass); + InterpreterResolvedJavaField field = iUniverse.field(staticField); + JavaConstant javaConstant = iUniverse.constant(JavaConstant.forBoolean(value)); + BuildTimeInterpreterUniverse.setUnmaterializedConstantValue(field, javaConstant); + field.markAsArtificiallyReachable(); + } + } + } + } + } + + OptionValues invocationLookupOptions = new OptionValues(EconomicMap.create()); + for (HostedMethod hMethod : hUniverse.getMethods()) { + AnalysisMethod aMethod = hMethod.getWrapped(); + if (isReachable(aMethod)) { + boolean needsMethodBody = InterpreterFeature.executableByInterpreter(aMethod); + // Test if the methods needs to be compiled for execution in the interpreter: + if (hMethod.hasBytecodes() && aMethod.getAnalyzedGraph() != null) { + if (aMethod.wrapped instanceof SubstitutionMethod subMethod && subMethod.isUserSubstitution() || + invocationPlugins.lookupInvocation(aMethod, invocationLookupOptions) != null) { + // The method is substituted, or an invocation plugin is registered + SubstrateCompilationDirectives.singleton().registerForcedCompilation(hMethod); + needsMethodBody = false; + } + } + BuildTimeInterpreterUniverse.singleton().method(aMethod, needsMethodBody, aMetaAccess); + } + } + + for (HostedField hField : accessImpl.getUniverse().getFields()) { + AnalysisField aField = hField.getWrapped(); + if (aField.isReachable()) { + BuildTimeInterpreterUniverse.singleton().field(aField); + } + } + + iUniverse.purgeUnreachable(hMetaAccess); + + for (HostedType hostedType : hUniverse.getTypes()) { + AnalysisType analysisType = hostedType.getWrapped(); + InterpreterResolvedJavaType iType = iUniverse.getType(analysisType); + + if (!(iType instanceof InterpreterResolvedObjectType objectType)) { + continue; + } + if (hostedType.getVTable().length == 0) { + continue; + } + + InterpreterResolvedJavaMethod[] iVTable = new InterpreterResolvedJavaMethod[hostedType.getVTable().length]; + + for (int i = 0; i < iVTable.length; i++) { + iVTable[i] = iUniverse.getMethod(hostedType.getVTable()[i].getWrapped()); + } + objectType.setVtable(iVTable); + } + + HostedMethod methodNotCompiledHandler = hMetaAccess.lookupJavaMethod(InvalidMethodPointerHandler.METHOD_POINTER_NOT_COMPILED_HANDLER_METHOD); + InterpreterMethodPointerHolder.setMethodNotCompiledHandler(new MethodPointer(methodNotCompiledHandler)); + + // Allow methods that call System.arraycopy to be interpreted. + try { + HostedMethod arraycopy = hMetaAccess.lookupJavaMethod( + System.class.getDeclaredMethod("arraycopy", Object.class, int.class, Object.class, int.class, int.class)); + SubstrateCompilationDirectives.singleton().registerForcedCompilation(arraycopy); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + @Override + public void afterCompilation(AfterCompilationAccess access) { + FeatureImpl.AfterCompilationAccessImpl accessImpl = (FeatureImpl.AfterCompilationAccessImpl) access; + + BuildTimeInterpreterUniverse.singleton().createConstantPools(accessImpl.getUniverse()); + + int estOffset = 0; + for (InterpreterResolvedJavaMethod interpreterMethod : BuildTimeInterpreterUniverse.singleton().getMethods()) { + HostedMethod hostedMethod = accessImpl.getUniverse().optionalLookup(interpreterMethod.getOriginalMethod()); + + CompileQueue.CompileTask compileTask = accessImpl.getCompilations().get(hostedMethod); + ResolvedJavaMethod[] inlinedMethods = compileTask == null ? null : compileTask.result.getMethods(); + + if (inlinedMethods == null) { + InterpreterUtil.log("[inlinedeps] Method %s doesn't have any inlinees", hostedMethod); + } else if (interpreterMethod.isInterpreterExecutable()) { + InterpreterUtil.log("[inlinedeps] Inlined methods for %s: %s", hostedMethod, inlinedMethods.length); + + // GR-55054: check if all inlined methods are reachable and included. + // There are a few exceptions, e.g. Substitutions + for (ResolvedJavaMethod inlinee : inlinedMethods) { + AnalysisMethod analysisMethod = ((HostedMethod) inlinee).getWrapped(); + JavaMethod inlineeJavaMethod = BuildTimeInterpreterUniverse.singleton().methodOrUnresolved(analysisMethod); + if (inlineeJavaMethod instanceof InterpreterResolvedJavaMethod inlineeInterpreterMethod) { + if (inlineeInterpreterMethod.equals(interpreterMethod)) { + InterpreterUtil.log("[inlinedeps] \t%s includes itself as an inlining dependency", interpreterMethod); + } else { + inlineeInterpreterMethod.addInliner(interpreterMethod); + InterpreterUtil.log("[inlinedeps] \t%s", inlinee); + } + } else { + InterpreterUtil.log("[inlinedeps] \tWarning: did not find interp method for %s", inlinee); + } + } + } + + if (!hostedMethod.isCompiled()) { + InterpreterUtil.log("[got] after compilation: %s is not compiled, nulling it out", hostedMethod); + interpreterMethod.setVTableIndex(VTBL_NO_ENTRY); + interpreterMethod.setNativeEntryPoint(null); + } else { + if (interpreterMethod.hasBytecodes()) { + /* only allocate stub for methods that we can actually run in the interpreter */ + interpreterMethod.setEnterStubOffset(estOffset++); + } + + interpreterMethod.setNativeEntryPoint(new MethodPointer(interpreterMethod.getOriginalMethod())); + } + + if (!interpreterMethod.isStatic() && !interpreterMethod.isConstructor()) { + if (hostedMethod.getImplementations().length > 1) { + if (!hostedMethod.hasVTableIndex()) { + InterpreterUtil.log("[vtable assignment] %s has multiple implementations but no vtable slot. This is not supported.\n", hostedMethod); + } else { + InterpreterUtil.log("[vtable assignment] Setting to Index %s for methods %s <> %s\n", hostedMethod.getVTableIndex(), interpreterMethod, hostedMethod); + interpreterMethod.setVTableIndex(hostedMethod.getVTableIndex()); + /* + * Do not null out native entry point, the method may be invoked via + * INVOKESPECIAL + */ + } + } else if (hostedMethod.getImplementations().length == 1) { + InterpreterUtil.log("[vtable assignment] Only one implementation available for %s\n", hostedMethod); + interpreterMethod.setVTableIndex(VTBL_ONE_IMPL); + + InterpreterResolvedJavaMethod oneImpl = (InterpreterResolvedJavaMethod) BuildTimeInterpreterUniverse.singleton().methodOrUnresolved(hostedMethod.getImplementations()[0]); + interpreterMethod.setOneImplementation(oneImpl); + InterpreterUtil.log("[vtable assignment] set oneImpl to -> %s\n", oneImpl); + } else { + InterpreterUtil.log("[vtable assignment] No implementation available: %s\n", hostedMethod); + interpreterMethod.setVTableIndex(VTBL_NO_ENTRY); + } + } + } + + NativeImageHeap heap = accessImpl.getHeap(); + + for (InterpreterResolvedJavaField field : BuildTimeInterpreterUniverse.singleton().getFields()) { + HostedField hostedField = accessImpl.getUniverse().optionalLookup(field.getOriginalField()); + if (!hostedField.isReachable()) { + // Field was not included in the image, so it can only be an artificially reachable + // field used only by the interpreter. These fields are read-only, thus + // unmaterialized, + // by now its value should be already computed. + VMError.guarantee(field.isArtificiallyReachable()); + VMError.guarantee(field.isUnmaterializedConstant()); + VMError.guarantee(field.getUnmaterializedConstant() != null); + } else if (hostedField.isUnmaterialized()) { + AnalysisField analysisField = (AnalysisField) field.getOriginalField(); + if (hostedField.getType().isWordType() || analysisField.getJavaKind().isPrimitive()) { + JavaConstant constant = heap.hConstantReflection.readFieldValue(hostedField, null); + assert constant instanceof PrimitiveConstant; + BuildTimeInterpreterUniverse.setUnmaterializedConstantValue(field, constant); + } else if (analysisField.isRead() || analysisField.isFolded()) { + // May or may not be in the image. + assert analysisField.getJavaKind().isObject(); + JavaConstant constantValue = heap.hConstantReflection.readFieldValue(hostedField, null); + BuildTimeInterpreterUniverse.setUnmaterializedConstantValue(field, constantValue); + } else { + // Block access. + BuildTimeInterpreterUniverse.setUnmaterializedConstantValue(field, JavaConstant.forIllegal()); + } + VMError.guarantee(field.isUnmaterializedConstant()); + } else if (!hostedField.hasLocation()) { + InterpreterUtil.log("Found materialized field without location: %s", hostedField); + BuildTimeInterpreterUniverse.setUnmaterializedConstantValue(field, JavaConstant.forIllegal()); + } else { + int fieldOffset = hostedField.getOffset(); + field.setOffset(fieldOffset); + } + } + + DebuggerSupport supportImpl = DebuggerSupport.singleton(); + for (InterpreterResolvedJavaMethod method : BuildTimeInterpreterUniverse.singleton().getMethods()) { + ReferenceConstant nativeEntryPointHolderConstant = method.getNativeEntryPointHolderConstant(); + if (nativeEntryPointHolderConstant != null) { + supportImpl.ensureMethodPointerIsInImage(nativeEntryPointHolderConstant.getReferent()); + } + } + } + + @Override + public void afterHeapLayout(AfterHeapLayoutAccess access) { + FeatureImpl.AfterHeapLayoutAccessImpl accessImpl = (FeatureImpl.AfterHeapLayoutAccessImpl) access; + NativeImageHeap heap = accessImpl.getHeap(); + + for (InterpreterResolvedJavaField field : BuildTimeInterpreterUniverse.singleton().getFields()) { + if (field.isArtificiallyReachable()) { + // Value should be already computed. + JavaConstant value = field.getUnmaterializedConstant(); + VMError.guarantee(value != null && value != JavaConstant.ILLEGAL); + continue; + } + HostedField hostedField = accessImpl.getMetaAccess().getUniverse().optionalLookup(field.getOriginalField()); + if (hostedField.isUnmaterialized()) { + AnalysisField analysisField = (AnalysisField) field.getOriginalField(); + if (hostedField.getType().isWordType()) { + // Ignore, words are stored as primitive values. + } else if ((analysisField.isFolded() && analysisField.getJavaKind().isObject())) { + JavaConstant constantValue = heap.hConstantReflection.readFieldValue(hostedField, null); + BuildTimeInterpreterUniverse.setUnmaterializedConstantValue(field, constantValue); + } + } + } + + GOTEntryAllocator gotEntryAllocator = HostedPLTGOTConfiguration.singleton().getGOTEntryAllocator(); + for (InterpreterResolvedJavaMethod interpreterMethod : BuildTimeInterpreterUniverse.singleton().getMethods()) { + HostedMethod hostedMethod = accessImpl.getMetaAccess().getUniverse().optionalLookup(interpreterMethod.getOriginalMethod()); + + int gotOffset = GOT_NO_ENTRY; + + if (interpreterMethod.isInterpreterExecutable()) { + gotOffset = gotEntryAllocator.queryGotEntry(hostedMethod); + } + + if (gotOffset == GOT_NO_ENTRY) { + InterpreterUtil.log("[got] Missing GOT offset for %s", interpreterMethod); + } else { + InterpreterUtil.log("[got] Got GOT offset=%s for %s", gotOffset, interpreterMethod); + } + interpreterMethod.setGOTOffset(gotOffset); + } + } + + @Override + public void afterAbstractImageCreation(AfterAbstractImageCreationAccess access) { + FeatureImpl.AfterAbstractImageCreationAccessImpl accessImpl = ((FeatureImpl.AfterAbstractImageCreationAccessImpl) access); + + List includedMethods = BuildTimeInterpreterUniverse.singleton().getMethods() + .stream() + .filter(m -> m.getEnterStubOffset() != EST_NO_ENTRY) + .collect(Collectors.toList()); + + /* create enter stubs */ + InterpreterStubSection stubSection = ImageSingletons.lookup(InterpreterStubSection.class); + stubSection.createInterpreterEnterStubSection(accessImpl.getImage(), includedMethods); + + /* populate EST */ + enterStubTable.installAdditionalInfoIntoImageObjectFile(accessImpl.getImage(), includedMethods); + } + + @Override + public void beforeImageWrite(BeforeImageWriteAccess access) { + FeatureImpl.BeforeImageWriteAccessImpl accessImpl = (FeatureImpl.BeforeImageWriteAccessImpl) access; + + // Serialize interpreter metadata. + NativeImageHeap heap = accessImpl.getImage().getHeap(); + + Map, DynamicHub> classToHub = new IdentityHashMap<>(); + for (NativeImageHeap.ObjectInfo info : heap.getObjects()) { + Object object = info.getObject(); + if (object instanceof DynamicHub) { + DynamicHub hub = (DynamicHub) object; + classToHub.put(hub.getHostedJavaClass(), hub); + } + } + + DebuggerSupport supportImpl = DebuggerSupport.singleton(); + SerializationContext.Builder builder = supportImpl.getUniverseSerializerBuilder() + .registerWriter(true, ReferenceConstant.class, Serializers.newReferenceConstantWriter(ref -> { + NativeImageHeap.ObjectInfo info = null; + if (ref instanceof Class) { + DynamicHub hub = classToHub.get(ref); + info = heap.getObjectInfo(hub); + } else if (ref instanceof ImageHeapConstant imageHeapConstant) { + info = heap.getConstantInfo(imageHeapConstant); + } else { + info = heap.getObjectInfo(ref); + } + + if (info == null) { + // avoid side-effects + String purgedObject = Objects.toIdentityString(ref); + InterpreterUtil.log("Constant not serialized in the image: %s", purgedObject); + return 0L; + } else { + return info.getOffset(); + } + })); + + Path destDir = NativeImageGenerator.generatedFiles(HostedOptionValues.singleton()); + + // Be explicit here: .metadata file is derived from + String imageFileName = accessImpl.getImageName(); + String extension = accessImpl.getImage().getImageKind().getFilenameSuffix(); + if (!imageFileName.endsWith(extension)) { + imageFileName += extension; + } + + String metadataFileName = MetadataUtil.metadataFileName(imageFileName); + Path metadataPath = destDir.resolve(metadataFileName); + + int crc32; + InterpreterUniverseImpl snapshot = BuildTimeInterpreterUniverse.singleton().snapshot(); + try { + snapshot.saveTo(builder, metadataPath); + crc32 = InterpreterUniverseImpl.computeCRC32(metadataPath); + } catch (IOException e) { + throw new RuntimeException(e); + } + + BuildArtifacts.singleton().add(BuildArtifacts.ArtifactType.DEBUG_INFO, metadataPath); + + String hashString = "crc32:" + toHexString(crc32); + + String dumpInterpreterClassFiles = InterpreterOptions.InterpreterDumpClassFiles.getValue(); + + if (!dumpInterpreterClassFiles.isEmpty()) { + try { + dumpInterpreterMetadataAsClassFiles(snapshot, dumpInterpreterClassFiles); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + InterpreterStubSection stubSection = ImageSingletons.lookup(InterpreterStubSection.class); + + stubSection.markEnterStubPatch(accessImpl.getHostedMetaAccess().lookupJavaMethod(enterInterpreterMethod)); + enterStubTable.writeMetadataHashString(hashString.getBytes(StandardCharsets.UTF_8)); + } + + @Platforms(Platform.HOSTED_ONLY.class) + @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "path.getParent() is never null") + private static void dumpInterpreterMetadataAsClassFiles(InterpreterUniverseImpl universe, String outputFolder) throws IOException { + for (InterpreterResolvedJavaType type : universe.getTypes()) { + if (type.isPrimitive() || type.isArray()) { + continue; + } + String typeName = type.getName(); + String separator = FileSystems.getDefault().getSeparator(); + assert typeName.startsWith("L") && typeName.endsWith(";"); + String relativeFilePath = typeName.substring(1, typeName.length() - 1).replace("/", separator) + ".class"; + Path path = Path.of(outputFolder, relativeFilePath); + if (!Files.exists(path.getParent())) { + Files.createDirectories(path.getParent()); + } + byte[] bytes = ClassFile.dumpInterpreterTypeClassFile(universe, type); + Files.write(path, bytes, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/DebuggerSupport.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/DebuggerSupport.java new file mode 100644 index 000000000000..ec28340f13af --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/DebuggerSupport.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import com.oracle.graal.pointsto.heap.ImageHeapConstant; +import com.oracle.svm.core.BuildPhaseProvider; +import com.oracle.svm.core.FunctionPointerHolder; +import com.oracle.svm.core.c.CGlobalData; +import com.oracle.svm.core.c.CGlobalDataFactory; +import com.oracle.svm.core.heap.UnknownObjectField; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; +import com.oracle.svm.interpreter.metadata.InterpreterUniverseImpl; +import com.oracle.svm.interpreter.metadata.Lazy; +import com.oracle.svm.interpreter.metadata.MetadataUtil; +import com.oracle.svm.interpreter.metadata.serialization.SerializationContext; +import com.oracle.svm.interpreter.metadata.serialization.Serializers; +import jdk.graal.compiler.api.replacements.Fold; +import jdk.graal.compiler.api.replacements.SnippetReflectionProvider; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; +import org.graalvm.collections.EconomicMap; +import org.graalvm.collections.MapCursor; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.ProcessProperties; +import org.graalvm.word.Pointer; + +import java.io.IOException; +import java.lang.reflect.Executable; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +import static com.oracle.svm.interpreter.InterpreterUtil.traceInterpreter; + +public class DebuggerSupport { + public static final String IMAGE_INTERP_HASH_SYMBOL_NAME = "__svm_interp_hash"; + + public static final CGlobalData IMAGE_INTERP_HASH = CGlobalDataFactory.forSymbol(IMAGE_INTERP_HASH_SYMBOL_NAME); + + private static final SerializationContext.Builder READER_BUILDER = Serializers.newBuilderForInterpreterMetadata(); + + private ArrayList referencesInImage = new ArrayList<>(); + + @UnknownObjectField(availability = BuildPhaseProvider.AfterCompilation.class) // + private final ArrayList methodPointersInImage = new ArrayList<>(); + + private final Lazy universe; + + @SuppressWarnings("this-escape") + public DebuggerSupport() { + this.universe = Lazy.of(() -> { + logForcedReferencesHistogram(this.referencesInImage, this.methodPointersInImage); + try { + return InterpreterUniverseImpl.loadFrom(getUniverseSerializerBuilder(), false, getMetadataHashString(), getMetadataFilePath()); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (Exception e) { + if (InterpreterOptions.InterpreterTraceSupport.getValue() && InterpreterOptions.InterpreterTrace.getValue()) { + e.printStackTrace(); + } + throw VMError.shouldNotReachHere(e); + } + }); + } + + @Fold + public static boolean isEnabled() { + return ImageSingletons.contains(DebuggerSupport.class); + } + + @Fold + public static DebuggerSupport singleton() { + return ImageSingletons.lookup(DebuggerSupport.class); + } + + public static Path getMetadataFilePath() { + return MetadataUtil.metadataFilePath(Path.of(ProcessProperties.getExecutableName())); + } + + public static String getMetadataHashString() { + Pointer base = IMAGE_INTERP_HASH.get(); + int length = base.readInt(0); + byte[] bytes = new byte[length]; + for (int i = 0; i < length; ++i) { + bytes[i] = base.readByte(4 + i); + } + return new String(bytes, StandardCharsets.UTF_8); + } + + private static void logForcedReferencesHistogram(ArrayList references, ArrayList methodPointers) { + traceInterpreter("Forced constants: ").signed(references.size()).newline(); + traceInterpreter("Forced method pointers: ").signed(methodPointers.size()).newline(); + traceInterpreter("Forced constants histogram:"); + EconomicMap, Integer> histogram = EconomicMap.create(); + for (Object object : references) { + histogram.put(object.getClass(), histogram.get(object.getClass(), 0) + 1); + } + MapCursor, Integer> cursor = histogram.getEntries(); + while (cursor.advance()) { + traceInterpreter(" ").string(cursor.getKey().toString()).string(" ").string(cursor.getValue().toString()).newline(); + } + } + + /** + * Returns the interpreter "type" for a specific {@link Class}, or null if it doesn't exist or + * if the liaison wasn't registered at build time. This is an internal API meant for + * debugging and testing purposes only. + */ + // GR-55023: should be in InterpreterSupport + public static ResolvedJavaType lookupType(Class declaringClass) { + return InterpreterDirectivesSupportImpl.getInterpreterType(declaringClass); + } + + /** + * Returns the interpreter "method" for a specific {@link Executable}, or null if it doesn't + * exist or if the liaison wasn't registered at build time. This is an internal API meant for + * debugging and testing purposes only. + */ + // GR-55023: should be in InterpreterSupport + public static ResolvedJavaMethod lookupMethod(ResolvedJavaType clazz, String methodName, Class returnType, Class... parameterTypes) { + VMError.guarantee(clazz instanceof InterpreterResolvedJavaType); + return InterpreterDirectivesSupportImpl.getInterpreterMethod((InterpreterResolvedJavaType) clazz, methodName, returnType, parameterTypes); + } + + @SuppressWarnings("static-method") + public SerializationContext.Builder getUniverseSerializerBuilder() { + return READER_BUILDER; + } + + // GR-55023: should be in InterpreterSupport + public InterpreterUniverse getUniverse() { + return universe.get(); + } + + @Platforms(Platform.HOSTED_ONLY.class) + void trimForcedReferencesInImageHeap() { + Set unique = Collections.newSetFromMap(new IdentityHashMap<>()); + unique.addAll(this.referencesInImage); + this.referencesInImage = new ArrayList<>(unique); + } + + @Platforms(Platform.HOSTED_ONLY.class) + void ensureConstantIsInImageHeap(SnippetReflectionProvider snippetReflectionProvider, ImageHeapConstant imageHeapConstant) { + if (imageHeapConstant.isBackedByHostedObject()) { + Object value = snippetReflectionProvider.asObject(Object.class, imageHeapConstant.getHostedObject()); + VMError.guarantee(value != null); + referencesInImage.add(value); + } else { + throw VMError.shouldNotReachHere("Constant is not backed: " + imageHeapConstant); + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void ensureMethodPointerIsInImage(FunctionPointerHolder value) { + if (value != null) { + methodPointersInImage.add(value); + } + } + + @Platforms(Platform.HOSTED_ONLY.class) + public void buildMethodIdMapping(ResolvedJavaMethod[] encodedMethods) { + assert encodedMethods[0] == null; + for (int i = 1; i < encodedMethods.length; i++) { + ResolvedJavaMethod method = encodedMethods[i]; + if (method != null) { + InterpreterResolvedJavaMethod interpreterMethod = BuildTimeInterpreterUniverse.singleton().getMethod(method); + interpreterMethod.setMethodId(i); + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/EspressoFrame.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/EspressoFrame.java new file mode 100644 index 000000000000..06f6b621dd38 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/EspressoFrame.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter; + +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterUnresolvedSignature; + +import jdk.vm.ci.meta.JavaKind; + +/** + * Exposes accessors to the Espresso frame e.g. operand stack, locals and current BCI. + */ +public final class EspressoFrame { + + private EspressoFrame() { + throw VMError.shouldNotReachHere("private constructor"); + } + + public static InterpreterFrame allocate(int maxLocals, int maxStackSize, Object... arguments) { + return InterpreterFrame.create(maxLocals + maxStackSize, arguments); + } + + // region Operand stack accessors + + public static void dup1(InterpreterFrame frame, int top) { + // value1 -> value1, value1 + copyStatic(frame, top - 1, top); + } + + public static void dupx1(InterpreterFrame frame, int top) { + // value2, value1 -> value1, value2, value1 + copyStatic(frame, top - 1, top); + copyStatic(frame, top - 2, top - 1); + copyStatic(frame, top, top - 2); + } + + public static void dupx2(InterpreterFrame frame, int top) { + // value3, value2, value1 -> value1, value3, value2, value1 + copyStatic(frame, top - 1, top); + copyStatic(frame, top - 2, top - 1); + copyStatic(frame, top - 3, top - 2); + copyStatic(frame, top, top - 3); + } + + public static void dup2(InterpreterFrame frame, int top) { + // {value2, value1} -> {value2, value1}, {value2, value1} + copyStatic(frame, top - 2, top); + copyStatic(frame, top - 1, top + 1); + } + + public static void swapSingle(InterpreterFrame frame, int top) { + // value2, value1 -> value1, value2 + swapStatic(frame, top); + } + + public static void dup2x1(InterpreterFrame frame, int top) { + // value3, {value2, value1} -> {value2, value1}, value3, {value2, value1} + copyStatic(frame, top - 2, top); + copyStatic(frame, top - 1, top + 1); + copyStatic(frame, top - 3, top - 1); + copyStatic(frame, top, top - 3); + copyStatic(frame, top + 1, top - 2); + } + + public static void dup2x2(InterpreterFrame frame, int top) { + // {value4, value3}, {value2, value1} -> {value2, value1}, {value4, value3}, {value2, + // value1} + copyStatic(frame, top - 1, top + 1); + copyStatic(frame, top - 2, top); + copyStatic(frame, top - 3, top - 1); + copyStatic(frame, top - 4, top - 2); + copyStatic(frame, top, top - 4); + copyStatic(frame, top + 1, top - 3); + } + + private static void swapStatic(InterpreterFrame frame, int top) { + frame.swapStatic(top - 1, top - 2); + } + + private static void copyStatic(InterpreterFrame frame, int src, int dst) { + frame.copyStatic(src, dst); + } + + public static int popInt(InterpreterFrame frame, int slot) { + int result = frame.getIntStatic(slot); + // Avoid keeping track of popped slots in FrameStates. + clearPrimitive(frame, slot); + return result; + } + + public static Object peekObject(InterpreterFrame frame, int slot) { + Object result = frame.getObjectStatic(slot); + return result; + } + + public static long peekPrimitive(InterpreterFrame frame, int slot) { + return frame.getLongStatic(slot); + } + + /** + * Reads and clear the operand stack slot. + */ + public static Object popObject(InterpreterFrame frame, int slot) { + // nulls-out the slot, use peekObject to read only + Object result = frame.getObjectStatic(slot); + clearReference(frame, slot); + assert !(result instanceof ReturnAddress); + return result; + } + + public static float popFloat(InterpreterFrame frame, int slot) { + float result = frame.getFloatStatic(slot); + // Avoid keeping track of popped slots in FrameStates. + clearPrimitive(frame, slot); + return result; + } + + public static long popLong(InterpreterFrame frame, int slot) { + long result = frame.getLongStatic(slot); + // Avoid keeping track of popped slots in FrameStates. + clearPrimitive(frame, slot); + return result; + } + + public static double popDouble(InterpreterFrame frame, int slot) { + double result = frame.getDoubleStatic(slot); + // Avoid keeping track of popped slots in FrameStates. + clearPrimitive(frame, slot); + return result; + } + + static Object popReturnAddressOrObject(InterpreterFrame frame, int slot) { + Object result = frame.getObjectStatic(slot); + clearReference(frame, slot); + return result; + } + + static void putReturnAddress(InterpreterFrame frame, int slot, int targetBCI) { + frame.setObjectStatic(slot, ReturnAddress.create(targetBCI)); + } + + public static void putObject(InterpreterFrame frame, int slot, Object value) { + frame.setObjectStatic(slot, value); + } + + public static void putInt(InterpreterFrame frame, int slot, int value) { + frame.setIntStatic(slot, value); + } + + public static void putFloat(InterpreterFrame frame, int slot, float value) { + frame.setFloatStatic(slot, value); + } + + public static void putLong(InterpreterFrame frame, int slot, long value) { + frame.setLongStatic(slot + 1, value); + } + + public static void putDouble(InterpreterFrame frame, int slot, double value) { + frame.setDoubleStatic(slot + 1, value); + } + + private static void clearReference(InterpreterFrame frame, int slot) { + frame.clearObjectStatic(slot); + } + + private static void clearPrimitive(InterpreterFrame frame, int slot) { + frame.clearPrimitiveStatic(slot); + } + + public static void clear(InterpreterFrame frame, int slot) { + frame.clearStatic(slot); + } + + // endregion Operand stack accessors + + // region Local accessors + + public static void clearLocal(InterpreterFrame frame, int localSlot) { + clear(frame, localSlot); + } + + public static void setLocalObject(InterpreterFrame frame, int localSlot, Object value) { + assert !(value instanceof ReturnAddress); + frame.setObjectStatic(localSlot, value); + } + + static void setLocalObjectOrReturnAddress(InterpreterFrame frame, int localSlot, Object value) { + frame.setObjectStatic(localSlot, value); + } + + public static void setLocalInt(InterpreterFrame frame, int localSlot, int value) { + frame.setIntStatic(localSlot, value); + } + + public static void setLocalFloat(InterpreterFrame frame, int localSlot, float value) { + frame.setFloatStatic(localSlot, value); + } + + public static void setLocalLong(InterpreterFrame frame, int localSlot, long value) { + frame.setLongStatic(localSlot, value); + } + + public static void setLocalDouble(InterpreterFrame frame, int localSlot, double value) { + frame.setDoubleStatic(localSlot, value); + } + + public static int getLocalInt(InterpreterFrame frame, int localSlot) { + return frame.getIntStatic(localSlot); + } + + public static Object getLocalObject(InterpreterFrame frame, int localSlot) { + Object result = frame.getObjectStatic(localSlot); + return result; + } + + public static Object getThis(InterpreterFrame frame) { + return getLocalObject(frame, 0); + } + + static int getLocalReturnAddress(InterpreterFrame frame, int localSlot) { + Object result = frame.getObjectStatic(localSlot); + assert result != null; + return ((ReturnAddress) result).bci(); + } + + public static float getLocalFloat(InterpreterFrame frame, int localSlot) { + return frame.getFloatStatic(localSlot); + } + + public static long getLocalLong(InterpreterFrame frame, int localSlot) { + return frame.getLongStatic(localSlot); + } + + public static double getLocalDouble(InterpreterFrame frame, int localSlot) { + return frame.getDoubleStatic(localSlot); + } + + // endregion Local accessors + + public static int startingStackOffset(int maxLocals) { + return maxLocals; + } + + public static Object[] popArguments(InterpreterFrame frame, int top, boolean hasReceiver, InterpreterUnresolvedSignature signature) { + int argCount = signature.getParameterCount(false); + + int extraParam = hasReceiver ? 1 : 0; + final Object[] args = new Object[argCount + extraParam]; + + int argAt = top - 1; + for (int i = argCount - 1; i >= 0; --i) { + JavaKind argKind = signature.getParameterKind(i); + // @formatter:off + switch (argKind) { + case Boolean: args[i + extraParam] = (popInt(frame, argAt) != 0); break; + case Byte: args[i + extraParam] = (byte) popInt(frame, argAt); break; + case Short: args[i + extraParam] = (short) popInt(frame, argAt); break; + case Char: args[i + extraParam] = (char) popInt(frame, argAt); break; + case Int: args[i + extraParam] = popInt(frame, argAt); break; + case Float: args[i + extraParam] = popFloat(frame, argAt); break; + case Long: args[i + extraParam] = popLong(frame, argAt); --argAt; break; + case Double: args[i + extraParam] = popDouble(frame, argAt); --argAt; break; + case Object: args[i + extraParam] = popObject(frame, argAt); break; + default: + throw VMError.shouldNotReachHere("implement me: " + argKind); + + } + // @formatter:on + --argAt; + } + if (hasReceiver) { + args[0] = popObject(frame, argAt); + } + return args; + } + + /** + * Puts a value in the operand stack. This method follows the JVM spec, where sub-word types (< + * int) are always treated as int. + * + * Returns the number of used slots. + * + * @param value value to push + * @param returnKind kind to push + */ + public static int putKind(InterpreterFrame frame, int top, Object value, JavaKind returnKind) { + // @formatter:off + switch (returnKind) { + case Boolean : putInt(frame, top, ((boolean) value) ? 1 : 0); break; + case Byte : putInt(frame, top, (byte) value); break; + case Short : putInt(frame, top, (short) value); break; + case Char : putInt(frame, top, (char) value); break; + case Int : putInt(frame, top, (int) value); break; + case Float : putFloat(frame, top, (float) value); break; + case Long : putLong(frame, top, (long) value); break; + case Double : putDouble(frame, top, (double) value); break; + case Object : putObject(frame, top, value); break; + case Void : /* ignore */ break; + default : + throw VMError.shouldNotReachHereAtRuntime(); + } + // @formatter:on + return returnKind.getSlotCount(); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/Interpreter.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/Interpreter.java new file mode 100644 index 000000000000..64b9bd0a3ed4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/Interpreter.java @@ -0,0 +1,1528 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import static com.oracle.svm.interpreter.EspressoFrame.clear; +import static com.oracle.svm.interpreter.EspressoFrame.dup1; +import static com.oracle.svm.interpreter.EspressoFrame.dup2; +import static com.oracle.svm.interpreter.EspressoFrame.dup2x1; +import static com.oracle.svm.interpreter.EspressoFrame.dup2x2; +import static com.oracle.svm.interpreter.EspressoFrame.dupx1; +import static com.oracle.svm.interpreter.EspressoFrame.dupx2; +import static com.oracle.svm.interpreter.EspressoFrame.getLocalDouble; +import static com.oracle.svm.interpreter.EspressoFrame.getLocalFloat; +import static com.oracle.svm.interpreter.EspressoFrame.getLocalInt; +import static com.oracle.svm.interpreter.EspressoFrame.getLocalLong; +import static com.oracle.svm.interpreter.EspressoFrame.getLocalObject; +import static com.oracle.svm.interpreter.EspressoFrame.getLocalReturnAddress; +import static com.oracle.svm.interpreter.EspressoFrame.peekObject; +import static com.oracle.svm.interpreter.EspressoFrame.popDouble; +import static com.oracle.svm.interpreter.EspressoFrame.popFloat; +import static com.oracle.svm.interpreter.EspressoFrame.popInt; +import static com.oracle.svm.interpreter.EspressoFrame.popLong; +import static com.oracle.svm.interpreter.EspressoFrame.popObject; +import static com.oracle.svm.interpreter.EspressoFrame.popReturnAddressOrObject; +import static com.oracle.svm.interpreter.EspressoFrame.putDouble; +import static com.oracle.svm.interpreter.EspressoFrame.putFloat; +import static com.oracle.svm.interpreter.EspressoFrame.putInt; +import static com.oracle.svm.interpreter.EspressoFrame.putLong; +import static com.oracle.svm.interpreter.EspressoFrame.putObject; +import static com.oracle.svm.interpreter.EspressoFrame.putReturnAddress; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalDouble; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalFloat; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalInt; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalLong; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalObject; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalObjectOrReturnAddress; +import static com.oracle.svm.interpreter.EspressoFrame.startingStackOffset; +import static com.oracle.svm.interpreter.EspressoFrame.swapSingle; +import static com.oracle.svm.interpreter.InterpreterUtil.traceInterpreter; +import static com.oracle.svm.interpreter.InterpreterToVM.nullCheck; +import static com.oracle.svm.interpreter.metadata.Bytecodes.AALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.AASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ACONST_NULL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ALOAD_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ALOAD_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ALOAD_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ALOAD_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ANEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ARETURN; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ARRAYLENGTH; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ASTORE_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ASTORE_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ASTORE_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ASTORE_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ATHROW; +import static com.oracle.svm.interpreter.metadata.Bytecodes.BALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.BASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.BIPUSH; +import static com.oracle.svm.interpreter.metadata.Bytecodes.BREAKPOINT; +import static com.oracle.svm.interpreter.metadata.Bytecodes.CALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.CASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.CHECKCAST; +import static com.oracle.svm.interpreter.metadata.Bytecodes.D2F; +import static com.oracle.svm.interpreter.metadata.Bytecodes.D2I; +import static com.oracle.svm.interpreter.metadata.Bytecodes.D2L; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DADD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DCMPG; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DCMPL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DCONST_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DCONST_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DDIV; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DLOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DLOAD_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DLOAD_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DLOAD_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DLOAD_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DMUL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DNEG; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DREM; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DRETURN; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DSTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DSTORE_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DSTORE_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DSTORE_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DSTORE_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DSUB; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DUP; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DUP2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DUP2_X1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DUP2_X2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DUP_X1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.DUP_X2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.F2D; +import static com.oracle.svm.interpreter.metadata.Bytecodes.F2I; +import static com.oracle.svm.interpreter.metadata.Bytecodes.F2L; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FADD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FCMPG; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FCMPL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FCONST_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FCONST_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FCONST_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FDIV; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FLOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FLOAD_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FLOAD_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FLOAD_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FLOAD_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FMUL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FNEG; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FREM; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FRETURN; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FSTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FSTORE_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FSTORE_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FSTORE_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FSTORE_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.FSUB; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GETFIELD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GETSTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GOTO; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GOTO_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.I2B; +import static com.oracle.svm.interpreter.metadata.Bytecodes.I2C; +import static com.oracle.svm.interpreter.metadata.Bytecodes.I2D; +import static com.oracle.svm.interpreter.metadata.Bytecodes.I2F; +import static com.oracle.svm.interpreter.metadata.Bytecodes.I2L; +import static com.oracle.svm.interpreter.metadata.Bytecodes.I2S; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IADD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IAND; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ICONST_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ICONST_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ICONST_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ICONST_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ICONST_4; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ICONST_5; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ICONST_M1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IDIV; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IFEQ; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IFGE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IFGT; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IFLE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IFLT; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IFNE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IFNONNULL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IFNULL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IF_ACMPEQ; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IF_ACMPNE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IF_ICMPEQ; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IF_ICMPGE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IF_ICMPGT; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IF_ICMPLE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IF_ICMPLT; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IF_ICMPNE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IINC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ILOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ILOAD_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ILOAD_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ILOAD_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ILOAD_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IMUL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INEG; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INSTANCEOF; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEDYNAMIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEINTERFACE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESPECIAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEVIRTUAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IOR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IREM; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IRETURN; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ISHL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ISHR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ISTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ISTORE_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ISTORE_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ISTORE_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ISTORE_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.ISUB; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IUSHR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.IXOR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.JSR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.JSR_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.L2D; +import static com.oracle.svm.interpreter.metadata.Bytecodes.L2F; +import static com.oracle.svm.interpreter.metadata.Bytecodes.L2I; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LADD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LAND; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LCMP; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LCONST_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LCONST_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC2_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDIV; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LLOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LLOAD_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LLOAD_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LLOAD_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LLOAD_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LMUL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LNEG; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LOOKUPSWITCH; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LOR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LREM; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LRETURN; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LSHL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LSHR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LSTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LSTORE_0; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LSTORE_1; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LSTORE_2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LSTORE_3; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LSUB; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LUSHR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LXOR; +import static com.oracle.svm.interpreter.metadata.Bytecodes.MONITORENTER; +import static com.oracle.svm.interpreter.metadata.Bytecodes.MONITOREXIT; +import static com.oracle.svm.interpreter.metadata.Bytecodes.MULTIANEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.NEW; +import static com.oracle.svm.interpreter.metadata.Bytecodes.NEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.NOP; +import static com.oracle.svm.interpreter.metadata.Bytecodes.POP; +import static com.oracle.svm.interpreter.metadata.Bytecodes.POP2; +import static com.oracle.svm.interpreter.metadata.Bytecodes.PUTFIELD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.PUTSTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.RET; +import static com.oracle.svm.interpreter.metadata.Bytecodes.RETURN; +import static com.oracle.svm.interpreter.metadata.Bytecodes.SALOAD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.SASTORE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.SIPUSH; +import static com.oracle.svm.interpreter.metadata.Bytecodes.SWAP; +import static com.oracle.svm.interpreter.metadata.Bytecodes.TABLESWITCH; +import static com.oracle.svm.interpreter.metadata.Bytecodes.WIDE; + +import com.oracle.svm.interpreter.debug.DebuggerEvents; +import com.oracle.svm.interpreter.debug.EventKind; +import com.oracle.svm.interpreter.debug.SteppingControl; +import com.oracle.svm.interpreter.metadata.BytecodeStream; +import com.oracle.svm.interpreter.metadata.LookupSwitch; +import com.oracle.svm.interpreter.metadata.ReferenceConstant; +import com.oracle.svm.interpreter.metadata.TableSwitch; +import jdk.graal.compiler.api.directives.GraalDirectives; +import com.oracle.svm.interpreter.metadata.Bytecodes; +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.svm.core.StaticFieldsSupport; +import com.oracle.svm.core.jdk.InternalVMMethod; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterConstantPool; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaField; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedObjectType; +import com.oracle.svm.interpreter.metadata.InterpreterUnresolvedSignature; +import com.oracle.svm.interpreter.metadata.MetadataUtil; + +import jdk.vm.ci.meta.ConstantPool; +import jdk.vm.ci.meta.ExceptionHandler; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaField; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.PrimitiveConstant; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.UnresolvedJavaType; + +/** + * Bytecode interpreter loop. + */ +@InternalVMMethod +public final class Interpreter { + protected static final String FAILURE_CONSTANT_NOT_PART_OF_IMAGE_HEAP = "Trying to load constant that is not part of the Native Image heap"; + + public static final DebuggerEvents DEBUGGER = MetadataUtil.requireNonNull(ImageSingletons.lookup(DebuggerEvents.class)); + + private Interpreter() { + throw VMError.shouldNotReachHere("private constructor"); + } + + private static void initArguments(InterpreterFrame frame, InterpreterResolvedJavaMethod method) { + Object[] arguments = frame.getArguments(); + + boolean hasReceiver = !method.isStatic(); + int receiverSlot = hasReceiver ? 1 : 0; + int curSlot = 0; + if (hasReceiver) { + assert arguments[0] != null : "null receiver in init arguments !"; + Object receiver = arguments[0]; + setLocalObject(frame, curSlot, receiver); + curSlot += JavaKind.Object.getSlotCount(); + } + + InterpreterUnresolvedSignature methodSignature = method.getSignature(); + for (int i = 0; i < methodSignature.getParameterCount(false); ++i) { + JavaKind argType = methodSignature.getParameterKind(i); + // @formatter:off + switch (argType) { + case Boolean: setLocalInt(frame, curSlot, ((boolean) arguments[i + receiverSlot]) ? 1 : 0); break; + case Byte: setLocalInt(frame, curSlot, ((byte) arguments[i + receiverSlot])); break; + case Short: setLocalInt(frame, curSlot, ((short) arguments[i + receiverSlot])); break; + case Char: setLocalInt(frame, curSlot, ((char) arguments[i + receiverSlot])); break; + case Int: setLocalInt(frame, curSlot, (int) arguments[i + receiverSlot]); break; + case Float: setLocalFloat(frame, curSlot, (float) arguments[i + receiverSlot]); break; + case Long: setLocalLong(frame, curSlot, (long) arguments[i + receiverSlot]); ++curSlot; break; + case Double: setLocalDouble(frame, curSlot, (double) arguments[i + receiverSlot]); ++curSlot; break; + case Object: + // Reference type. + Object argument = arguments[i + receiverSlot]; + setLocalObject(frame, curSlot, argument); + break; + default : + throw VMError.shouldNotReachHereAtRuntime(); + } + // @formatter:on + ++curSlot; + } + } + + public static void initializeFrame(InterpreterFrame frame, InterpreterResolvedJavaMethod method) { + initArguments(frame, method); + } + + public static Object execute(InterpreterResolvedJavaMethod method, Object[] args) { + return execute(method, args, false); + } + + public static Object execute(InterpreterResolvedJavaMethod method, InterpreterFrame frame) { + return execute0(method, frame, false); + } + + public static Object execute(InterpreterResolvedJavaMethod method, Object[] args, boolean forceStayInInterpreter) { + InterpreterFrame frame = EspressoFrame.allocate(method.getMaxLocals(), method.getMaxStackSize(), args); + + InterpreterUtil.guarantee(!method.isNative(), "trying to interpret native method %s", method); + + initializeFrame(frame, method); + return execute0(method, frame, forceStayInInterpreter); + } + + private static Object execute0(InterpreterResolvedJavaMethod method, InterpreterFrame frame, boolean stayInInterpreter) { + try { + if (method.isSynchronized()) { + Object lockTarget = method.isStatic() + ? method.getDeclaringClass().getJavaClass() + : EspressoFrame.getThis(frame); + assert lockTarget != null; + InterpreterToVM.monitorEnter(frame, nullCheck(lockTarget)); + } + int startTop = startingStackOffset(method.getMaxLocals()); + return Root.executeBodyFromBCI(frame, method, 0, startTop, stayInInterpreter); + } finally { + InterpreterToVM.releaseInterpreterFrameLocks(frame); + } + } + + public static final ThreadLocal logIndent = ThreadLocal.withInitial(() -> 0); + + private static int getLogIndent() { + if (InterpreterOptions.InterpreterTraceSupport.getValue()) { + return logIndent.get(); + } + return 0; + } + + private static void setLogIndent(int indent) { + if (InterpreterOptions.InterpreterTraceSupport.getValue()) { + logIndent.set(indent); + } + } + + private static void traceInterpreterEnter(InterpreterResolvedJavaMethod method, int indent, int curBCI, int top) { + /* arguments to Log methods might have side-effects */ + if (!InterpreterOptions.InterpreterTraceSupport.getValue()) { + return; + } + + setLogIndent(indent + 2); + traceInterpreter(" ".repeat(indent)) + .string("[interp] Entered ") + .string(method.getDeclaringClass().getName()) + .string("::") + .string(method.getName()) + .string(method.getSignature().toMethodDescriptor()) + .string(" with bci=").unsigned(curBCI) + .string("/top=").unsigned(top).newline(); + } + + private static void traceInterpreterReturn(InterpreterResolvedJavaMethod method, int indent, int curBCI, int top) { + /* arguments to Log methods might have side-effects */ + if (!InterpreterOptions.InterpreterTraceSupport.getValue()) { + return; + } + + setLogIndent(indent); + traceInterpreter(" ".repeat(indent)); + traceInterpreter("[interp] Leave ") + .string(method.getDeclaringClass().getName()) + .string("::") + .string(method.getName()) + .string(method.getSignature().toMethodDescriptor()) + .string(" with bci=").unsigned(curBCI) + .string("/top=").unsigned(top).newline(); + } + + private static void traceInterpreterInstruction(InterpreterFrame frame, int indent, int curBCI, int top, int curOpcode) { + /* arguments to Log methods might have side-effects */ + if (!InterpreterOptions.InterpreterTraceSupport.getValue()) { + return; + } + + traceInterpreter(" ".repeat(indent)) + .string("bci=").unsigned(curBCI).string(" ") + .string(Bytecodes.nameOf(curOpcode)); + for (int slot = top - 1; slot >= 0; slot--) { + traceInterpreter(", s").unsigned(slot).string("=").hex(frame.getLongStatic(slot)).string("/").object(frame.getObjectStatic(slot)); + } + traceInterpreter("").newline(); + } + + private static void traceInterpreterException(InterpreterResolvedJavaMethod method, int indent, int curBCI, int top) { + /* arguments to Log methods might have side-effects */ + if (!InterpreterOptions.InterpreterTraceSupport.getValue()) { + return; + } + + setLogIndent(indent); + traceInterpreter(" ".repeat(indent)) + .string("[interp] Exception ") + .string(method.getDeclaringClass().getName()) + .string("::") + .string(method.getName()) + .string(method.getSignature().toMethodDescriptor()) + .string(" with bci=").unsigned(curBCI) + .string("/top=").unsigned(top).newline(); + } + + public static final class Root { + + private static Object executeBodyFromBCI(InterpreterFrame frame, InterpreterResolvedJavaMethod method, int startBCI, int startTop, + boolean forceStayInInterpreter) { + int curBCI = startBCI; + int top = startTop; + byte[] code = method.getInterpretedCode(); + + assert method.isStatic() || EspressoFrame.getThis(frame) != null; + + InterpreterUtil.guarantee(code != null, "no bytecode stream for %s", method); + + int indent = getLogIndent(); + traceInterpreterEnter(method, indent, curBCI, top); + + int debuggerEventFlags = 0; + if (DEBUGGER.isEventEnabled(Thread.currentThread(), EventKind.METHOD_ENTRY)) { + if (((InterpreterResolvedJavaType) method.getDeclaringClass()).isMethodEnterEvent()) { + debuggerEventFlags |= EventKind.METHOD_ENTRY.getFlag(); + } + } + + loop: while (true) { + /* + * Opaque read ensuring that BREAKPOINT opcodes are eventually read. Opaque == plain + * on x86/64, but on other architectures the read must be eventually guaranteed. + */ + int curOpcode = BytecodeStream.opaqueOpcode(code, curBCI); + + if (DEBUGGER.isEventEnabled(Thread.currentThread(), EventKind.SINGLE_STEP)) { + // Check that stepping "depth" and "size" are respected. + Thread currentThread = Thread.currentThread(); + SteppingControl steppingControl = DEBUGGER.getSteppingControl(currentThread); + if (steppingControl != null && steppingControl.isActiveAtCurrentFrameDepth()) { + int stepSize = steppingControl.getSize(); + if (stepSize == SteppingControl.STEP_MIN || + (stepSize == SteppingControl.STEP_LINE && !steppingControl.withinSameLine(method, curBCI))) { + debuggerEventFlags |= EventKind.SINGLE_STEP.getFlag(); + } + } + } + + if (curOpcode == BREAKPOINT) { + if (DEBUGGER.isEventEnabled(Thread.currentThread(), EventKind.BREAKPOINT)) { + debuggerEventFlags |= EventKind.BREAKPOINT.getFlag(); + } + curOpcode = method.getOriginalOpcodeAt(curBCI); + } + if (debuggerEventFlags != 0) { + // We have possibly: method enter, step before statement/expression, breakpoint + DEBUGGER.getEventHandler().onEventAt(Thread.currentThread(), method, curBCI, null, debuggerEventFlags); + debuggerEventFlags = 0; + } + + try { + traceInterpreterInstruction(frame, indent, curBCI, top, curOpcode); + + // @formatter:off + switch (curOpcode) { + case NOP: break; + case ACONST_NULL: putObject(frame, top, null); break; + + case ICONST_M1: // fall through + case ICONST_0: // fall through + case ICONST_1: // fall through + case ICONST_2: // fall through + case ICONST_3: // fall through + case ICONST_4: // fall through + case ICONST_5: putInt(frame, top, curOpcode - ICONST_0); break; + + case LCONST_0: // fall through + case LCONST_1: putLong(frame, top, curOpcode - LCONST_0); break; + + case FCONST_0: // fall through + case FCONST_1: // fall through + case FCONST_2: putFloat(frame, top, curOpcode - FCONST_0); break; + + case DCONST_0: // fall through + case DCONST_1: putDouble(frame, top, curOpcode - DCONST_0); break; + + case BIPUSH: putInt(frame, top, BytecodeStream.readByte(code, curBCI)); break; + case SIPUSH: putInt(frame, top, BytecodeStream.readShort(code, curBCI)); break; + + case LDC : loadConstant(frame, method, top, BytecodeStream.readCPI1(code, curBCI), curOpcode); break; + case LDC_W : // fall through + case LDC2_W: loadConstant(frame, method, top, BytecodeStream.readCPI2(code, curBCI), curOpcode); break; + + case ILOAD: putInt(frame, top, getLocalInt(frame, BytecodeStream.readLocalIndex1(code, curBCI))); break; + case LLOAD: putLong(frame, top, getLocalLong(frame, BytecodeStream.readLocalIndex1(code, curBCI))); break; + case FLOAD: putFloat(frame, top, getLocalFloat(frame, BytecodeStream.readLocalIndex1(code, curBCI))); break; + case DLOAD: putDouble(frame, top, getLocalDouble(frame, BytecodeStream.readLocalIndex1(code, curBCI))); break; + case ALOAD: putObject(frame, top, getLocalObject(frame, BytecodeStream.readLocalIndex1(code, curBCI))); break; + + case ILOAD_0: // fall through + case ILOAD_1: // fall through + case ILOAD_2: // fall through + case ILOAD_3: putInt(frame, top, getLocalInt(frame, curOpcode - ILOAD_0)); break; + + case LLOAD_0: // fall through + case LLOAD_1: // fall through + case LLOAD_2: // fall through + case LLOAD_3: putLong(frame, top, getLocalLong(frame, curOpcode - LLOAD_0)); break; + + case FLOAD_0: // fall through + case FLOAD_1: // fall through + case FLOAD_2: // fall through + case FLOAD_3: putFloat(frame, top, getLocalFloat(frame, curOpcode - FLOAD_0)); break; + + case DLOAD_0: // fall through + case DLOAD_1: // fall through + case DLOAD_2: // fall through + case DLOAD_3: putDouble(frame, top, getLocalDouble(frame, curOpcode - DLOAD_0)); break; + + case ALOAD_0: putObject(frame, top, getLocalObject(frame, 0)); break; + case ALOAD_1: // fall through + case ALOAD_2: // fall through + case ALOAD_3: putObject(frame, top, getLocalObject(frame, curOpcode - ALOAD_0)); break; + + case IALOAD: // fall through + case LALOAD: // fall through + case FALOAD: // fall through + case DALOAD: // fall through + case BALOAD: // fall through + case CALOAD: // fall through + case SALOAD: // fall through + case AALOAD: arrayLoad(frame, top, curOpcode); break; + + case ISTORE: setLocalInt(frame, BytecodeStream.readLocalIndex1(code, curBCI), popInt(frame, top - 1)); break; + case LSTORE: setLocalLong(frame, BytecodeStream.readLocalIndex1(code, curBCI), popLong(frame, top - 1)); break; + case FSTORE: setLocalFloat(frame, BytecodeStream.readLocalIndex1(code, curBCI), popFloat(frame, top - 1)); break; + case DSTORE: setLocalDouble(frame, BytecodeStream.readLocalIndex1(code, curBCI), popDouble(frame, top - 1)); break; + case ASTORE: setLocalObjectOrReturnAddress(frame, BytecodeStream.readLocalIndex1(code, curBCI), popReturnAddressOrObject(frame, top - 1)); break; + + case ISTORE_0: // fall through + case ISTORE_1: // fall through + case ISTORE_2: // fall through + case ISTORE_3: setLocalInt(frame, curOpcode - ISTORE_0, popInt(frame, top - 1)); break; + + case LSTORE_0: // fall through + case LSTORE_1: // fall through + case LSTORE_2: // fall through + case LSTORE_3: setLocalLong(frame, curOpcode - LSTORE_0, popLong(frame, top - 1)); break; + + case FSTORE_0: // fall through + case FSTORE_1: // fall through + case FSTORE_2: // fall through + case FSTORE_3: setLocalFloat(frame, curOpcode - FSTORE_0, popFloat(frame, top - 1)); break; + + case DSTORE_0: // fall through + case DSTORE_1: // fall through + case DSTORE_2: // fall through + case DSTORE_3: setLocalDouble(frame, curOpcode - DSTORE_0, popDouble(frame, top - 1)); break; + + case ASTORE_0: // fall through + case ASTORE_1: // fall through + case ASTORE_2: // fall through + case ASTORE_3: setLocalObjectOrReturnAddress(frame, curOpcode - ASTORE_0, popReturnAddressOrObject(frame, top - 1)); break; + + case IASTORE: // fall through + case LASTORE: // fall through + case FASTORE: // fall through + case DASTORE: // fall through + case AASTORE: // fall through + case BASTORE: // fall through + case CASTORE: // fall through + case SASTORE: arrayStore(frame, top, curOpcode); break; + + case POP2: + clear(frame, top - 1); + clear(frame, top - 2); + break; + + case POP: clear(frame, top - 1); break; + + case DUP : dup1(frame, top); break; + case DUP_X1 : dupx1(frame, top); break; + case DUP_X2 : dupx2(frame, top); break; + case DUP2 : dup2(frame, top); break; + case DUP2_X1 : dup2x1(frame, top); break; + case DUP2_X2 : dup2x2(frame, top); break; + case SWAP : swapSingle(frame, top); break; + + case IADD: putInt(frame, top - 2, popInt(frame, top - 1) + popInt(frame, top - 2)); break; + case LADD: putLong(frame, top - 4, popLong(frame, top - 1) + popLong(frame, top - 3)); break; + case FADD: putFloat(frame, top - 2, popFloat(frame, top - 1) + popFloat(frame, top - 2)); break; + case DADD: putDouble(frame, top - 4, popDouble(frame, top - 1) + popDouble(frame, top - 3)); break; + + case ISUB: putInt(frame, top - 2, popInt(frame, top - 2) - popInt(frame, top - 1)); break; + case LSUB: putLong(frame, top - 4, popLong(frame, top - 3) - popLong(frame, top - 1)); break; + case FSUB: putFloat(frame, top - 2, popFloat(frame, top - 2) - popFloat(frame, top - 1)); break; + case DSUB: putDouble(frame, top - 4, popDouble(frame, top - 3) - popDouble(frame, top - 1)); break; + + case IMUL: putInt(frame, top - 2, popInt(frame, top - 1) * popInt(frame, top - 2)); break; + case LMUL: putLong(frame, top - 4, popLong(frame, top - 1) * popLong(frame, top - 3)); break; + case FMUL: putFloat(frame, top - 2, popFloat(frame, top - 1) * popFloat(frame, top - 2)); break; + case DMUL: putDouble(frame, top - 4, popDouble(frame, top - 1) * popDouble(frame, top - 3)); break; + + case IDIV: putInt(frame, top - 2, divInt(popInt(frame, top - 1), popInt(frame, top - 2))); break; + case LDIV: putLong(frame, top - 4, divLong(popLong(frame, top - 1), popLong(frame, top - 3))); break; + case FDIV: putFloat(frame, top - 2, divFloat(popFloat(frame, top - 1), popFloat(frame, top - 2))); break; + case DDIV: putDouble(frame, top - 4, divDouble(popDouble(frame, top - 1), popDouble(frame, top - 3))); break; + + case IREM: putInt(frame, top - 2, remInt(popInt(frame, top - 1), popInt(frame, top - 2))); break; + case LREM: putLong(frame, top - 4, remLong(popLong(frame, top - 1), popLong(frame, top - 3))); break; + case FREM: putFloat(frame, top - 2, remFloat(popFloat(frame, top - 1), popFloat(frame, top - 2))); break; + case DREM: putDouble(frame, top - 4, remDouble(popDouble(frame, top - 1), popDouble(frame, top - 3))); break; + + case INEG: putInt(frame, top - 1, -popInt(frame, top - 1)); break; + case LNEG: putLong(frame, top - 2, -popLong(frame, top - 1)); break; + case FNEG: putFloat(frame, top - 1, -popFloat(frame, top - 1)); break; + case DNEG: putDouble(frame, top - 2, -popDouble(frame, top - 1)); break; + + case ISHL: putInt(frame, top - 2, shiftLeftInt(popInt(frame, top - 1), popInt(frame, top - 2))); break; + case LSHL: putLong(frame, top - 3, shiftLeftLong(popInt(frame, top - 1), popLong(frame, top - 2))); break; + case ISHR: putInt(frame, top - 2, shiftRightSignedInt(popInt(frame, top - 1), popInt(frame, top - 2))); break; + case LSHR: putLong(frame, top - 3, shiftRightSignedLong(popInt(frame, top - 1), popLong(frame, top - 2))); break; + case IUSHR: putInt(frame, top - 2, shiftRightUnsignedInt(popInt(frame, top - 1), popInt(frame, top - 2))); break; + case LUSHR: putLong(frame, top - 3, shiftRightUnsignedLong(popInt(frame, top - 1), popLong(frame, top - 2))); break; + + case IAND: putInt(frame, top - 2, popInt(frame, top - 1) & popInt(frame, top - 2)); break; + case LAND: putLong(frame, top - 4, popLong(frame, top - 1) & popLong(frame, top - 3)); break; + + case IOR: putInt(frame, top - 2, popInt(frame, top - 1) | popInt(frame, top - 2)); break; + case LOR: putLong(frame, top - 4, popLong(frame, top - 1) | popLong(frame, top - 3)); break; + + case IXOR: putInt(frame, top - 2, popInt(frame, top - 1) ^ popInt(frame, top - 2)); break; + case LXOR: putLong(frame, top - 4, popLong(frame, top - 1) ^ popLong(frame, top - 3)); break; + + case IINC: + setLocalInt(frame, BytecodeStream.readLocalIndex1(code, curBCI), getLocalInt(frame, BytecodeStream.readLocalIndex1(code, curBCI)) + BytecodeStream.readIncrement1(code, curBCI)); + break; + + case I2L: putLong(frame, top - 1, popInt(frame, top - 1)); break; + case I2F: putFloat(frame, top - 1, popInt(frame, top - 1)); break; + case I2D: putDouble(frame, top - 1, popInt(frame, top - 1)); break; + + case L2I: putInt(frame, top - 2, (int) popLong(frame, top - 1)); break; + case L2F: putFloat(frame, top - 2, popLong(frame, top - 1)); break; + case L2D: putDouble(frame, top - 2, popLong(frame, top - 1)); break; + + case F2I: putInt(frame, top - 1, (int) popFloat(frame, top - 1)); break; + case F2L: putLong(frame, top - 1, (long) popFloat(frame, top - 1)); break; + case F2D: putDouble(frame, top - 1, popFloat(frame, top - 1)); break; + + case D2I: putInt(frame, top - 2, (int) popDouble(frame, top - 1)); break; + case D2L: putLong(frame, top - 2, (long) popDouble(frame, top - 1)); break; + case D2F: putFloat(frame, top - 2, (float) popDouble(frame, top - 1)); break; + + case I2B: putInt(frame, top - 1, (byte) popInt(frame, top - 1)); break; + case I2C: putInt(frame, top - 1, (char) popInt(frame, top - 1)); break; + case I2S: putInt(frame, top - 1, (short) popInt(frame, top - 1)); break; + + case LCMP : putInt(frame, top - 4, compareLong(popLong(frame, top - 1), popLong(frame, top - 3))); break; + case FCMPL: putInt(frame, top - 2, compareFloatLess(popFloat(frame, top - 1), popFloat(frame, top - 2))); break; + case FCMPG: putInt(frame, top - 2, compareFloatGreater(popFloat(frame, top - 1), popFloat(frame, top - 2))); break; + case DCMPL: putInt(frame, top - 4, compareDoubleLess(popDouble(frame, top - 1), popDouble(frame, top - 3))); break; + case DCMPG: putInt(frame, top - 4, compareDoubleGreater(popDouble(frame, top - 1), popDouble(frame, top - 3))); break; + + // @formatter:on + case IFEQ: // fall through + case IFNE: // fall through + case IFLT: // fall through + case IFGE: // fall through + case IFGT: // fall through + case IFLE: + if (takeBranchPrimitive1(popInt(frame, top - 1), curOpcode)) { + top += ConstantBytecodes.stackEffectOf(IFLE); + curBCI = beforeJumpChecks(frame, curBCI, BytecodeStream.readBranchDest2(code, curBCI), top); + continue loop; + } + break; + + case IF_ICMPEQ: // fall through + case IF_ICMPNE: // fall through + case IF_ICMPLT: // fall through + case IF_ICMPGE: // fall through + case IF_ICMPGT: // fall through + case IF_ICMPLE: + if (takeBranchPrimitive2(popInt(frame, top - 1), popInt(frame, top - 2), curOpcode)) { + top += ConstantBytecodes.stackEffectOf(IF_ICMPLE); + curBCI = beforeJumpChecks(frame, curBCI, BytecodeStream.readBranchDest2(code, curBCI), top); + continue loop; + } + break; + + case IF_ACMPEQ: // fall through + case IF_ACMPNE: + if (takeBranchRef2(popObject(frame, top - 1), popObject(frame, top - 2), curOpcode)) { + top += ConstantBytecodes.stackEffectOf(IF_ACMPNE); + curBCI = beforeJumpChecks(frame, curBCI, BytecodeStream.readBranchDest2(code, curBCI), top); + continue loop; + } + break; + + case IFNULL: // fall through + case IFNONNULL: + if (takeBranchRef1(popObject(frame, top - 1), curOpcode)) { + top += ConstantBytecodes.stackEffectOf(IFNONNULL); + curBCI = beforeJumpChecks(frame, curBCI, BytecodeStream.readBranchDest2(code, curBCI), top); + continue loop; + } + break; + + case GOTO: + curBCI = beforeJumpChecks(frame, curBCI, BytecodeStream.readBranchDest2(code, curBCI), top); + continue loop; + + case GOTO_W: + curBCI = beforeJumpChecks(frame, curBCI, BytecodeStream.readBranchDest4(code, curBCI), top); + continue loop; + + case JSR: { + putReturnAddress(frame, top, curBCI + ConstantBytecodes.lengthOf(JSR)); + // The JSR stack effect is incorrectly set to 0 in the compiler sources. + // To keep interpreter and compiler in sync, the correct stack effect is + // hardcoded here. + int stackEffect = 1; // Bytecodes.stackEffectOf(JSR) + top += stackEffect; + curBCI = beforeJumpChecks(frame, curBCI, BytecodeStream.readBranchDest2(code, curBCI), top); + continue loop; + } + case JSR_W: { + putReturnAddress(frame, top, curBCI + ConstantBytecodes.lengthOf(JSR_W)); + // The JSR_W stack effect is incorrectly set to 0 in the compiler + // sources. To keep interpreter and compiler in sync, the correct stack + // effect is hardcoded here. + int stackEffect = 1; // Bytecodes.stackEffectOf(JSR_W) + top += stackEffect; + curBCI = beforeJumpChecks(frame, curBCI, BytecodeStream.readBranchDest4(code, curBCI), top); + continue loop; + } + case RET: { + top += ConstantBytecodes.stackEffectOf(RET); + curBCI = beforeJumpChecks(frame, curBCI, getLocalReturnAddress(frame, BytecodeStream.readLocalIndex1(code, curBCI)), top); + continue loop; + } + + case TABLESWITCH: { + int index = popInt(frame, top - 1); + int low = TableSwitch.lowKey(code, curBCI); + int high = TableSwitch.highKey(code, curBCI); + assert low <= high; + + int targetBCI; + if (low <= index && index <= high) { + targetBCI = TableSwitch.targetAt(code, curBCI, index - low); + } else { + targetBCI = TableSwitch.defaultTarget(code, curBCI); + } + top += ConstantBytecodes.stackEffectOf(TABLESWITCH); + curBCI = beforeJumpChecks(frame, curBCI, targetBCI, top); + continue loop; + } + case LOOKUPSWITCH: { + int key = popInt(frame, top - 1); + int low = 0; + int high = LookupSwitch.numberOfCases(code, curBCI) - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + int midVal = LookupSwitch.keyAt(code, curBCI, mid); + if (midVal < key) { + low = mid + 1; + } else if (midVal > key) { + high = mid - 1; + } else { + // Key found. + int targetBCI = curBCI + LookupSwitch.offsetAt(code, curBCI, mid); + top += ConstantBytecodes.stackEffectOf(LOOKUPSWITCH); + curBCI = beforeJumpChecks(frame, curBCI, targetBCI, top); + continue loop; + } + } + + // Key not found. + int targetBCI = LookupSwitch.defaultTarget(code, curBCI); + top += ConstantBytecodes.stackEffectOf(LOOKUPSWITCH); + curBCI = beforeJumpChecks(frame, curBCI, targetBCI, top); + continue loop; + } + + case IRETURN: // fall through + case LRETURN: // fall through + case FRETURN: // fall through + case DRETURN: // fall through + case ARETURN: // fall through + case RETURN: { + Object returnValue = getReturnValueAsObject(frame, method, top); + traceInterpreterReturn(method, indent, curBCI, top); + if (DEBUGGER.isEventEnabled(Thread.currentThread(), EventKind.METHOD_EXIT)) { + if (((InterpreterResolvedJavaType) method.getDeclaringClass()).isMethodExitEvent()) { + int flags = EventKind.METHOD_EXIT.getFlag() | EventKind.METHOD_EXIT_WITH_RETURN_VALUE.getFlag(); + DEBUGGER.getEventHandler().onEventAt(Thread.currentThread(), method, curBCI, returnValue, flags); + } + } + return returnValue; + } + // @formatter:off + // Bytecodes order is shuffled. + case GETSTATIC : // fall through + case GETFIELD : top += getField(frame, top, resolveField(method, curOpcode, BytecodeStream.readCPI2(code, curBCI)), curOpcode); break; + case PUTSTATIC : // fall through + case PUTFIELD : top += putField(frame, top, resolveField(method, curOpcode, BytecodeStream.readCPI2(code, curBCI)), curOpcode); break; + + case INVOKEVIRTUAL : // fall through + case INVOKESPECIAL : // fall through + case INVOKESTATIC : // fall through + case INVOKEINTERFACE : // fall through + case INVOKEDYNAMIC : { + boolean preferStayInInterpreter = forceStayInInterpreter; + SteppingControl steppingControl = null; + boolean stepEventDisabled = false; + Thread currentThread = Thread.currentThread(); + if (DEBUGGER.isEventEnabled(currentThread, EventKind.SINGLE_STEP)) { + // Disable stepping for inner frames, except for step into, where we must force interpreter execution. + steppingControl = DEBUGGER.getSteppingControl(currentThread); + if (steppingControl != null) { + // If step events can be ignored at frame n => can be also ignored at inner frame n + 1. + steppingControl.pushFrame(); + if (!steppingControl.isActiveAtCurrentFrameDepth()) { + DEBUGGER.setEventEnabled(currentThread, EventKind.SINGLE_STEP, false); + stepEventDisabled = true; + } + if (steppingControl.getDepth() == SteppingControl.STEP_INTO) { + // For now force the callee to stay in interpreter. + // If this is not possible, the next step event will be triggered only after returning. + // From the debugger's perspective there's almost no difference between a compiled method and a native method. + preferStayInInterpreter = true; + } + } + } + + try { + top += invoke(frame, method, code, top, curBCI, curOpcode, forceStayInInterpreter, preferStayInInterpreter); + } finally { + SteppingControl newSteppingControl = DEBUGGER.getSteppingControl(currentThread); + if (newSteppingControl != null) { + if (DEBUGGER.isEventEnabled(currentThread, EventKind.SINGLE_STEP)) { + newSteppingControl.popFrame(); + } else if (steppingControl == newSteppingControl && stepEventDisabled) { + // Re-enable stepping events that could have been disabled by step outer/out into inner frames. + DEBUGGER.setEventEnabled(currentThread, EventKind.SINGLE_STEP, true); + newSteppingControl.popFrame(); + } + } + } + break; + } + + case NEW : putObject(frame, top, InterpreterToVM.createNewReference(resolveType(method, NEW, BytecodeStream.readCPI2(code, curBCI)))); break; + case NEWARRAY : putObject(frame, top - 1, InterpreterToVM.createNewPrimitiveArray(BytecodeStream.readByte(code, curBCI), popInt(frame, top - 1))); break; + case ANEWARRAY : putObject(frame, top - 1, InterpreterToVM.createNewReferenceArray(resolveType(method, ANEWARRAY, BytecodeStream.readCPI2(code, curBCI)), popInt(frame, top - 1))); break; + case ARRAYLENGTH : putInt(frame, top - 1, InterpreterToVM.arrayLength(nullCheck(popObject(frame, top - 1)))); break; + case ATHROW : + throw SemanticJavaException.raise((Throwable) nullCheck(popObject(frame, top - 1))); + + case CHECKCAST : { + Object receiver = peekObject(frame, top - 1); + // Resolve type iff receiver != null. + if (receiver != null) { + InterpreterToVM.checkCast(receiver, resolveType(method, CHECKCAST, BytecodeStream.readCPI2(code, curBCI))); + } + break; + } + case INSTANCEOF : { + Object receiver = popObject(frame, top - 1); + // Resolve type iff receiver != null. + putInt(frame, top - 1, (receiver != null && InterpreterToVM.instanceOf(receiver, resolveType(method, INSTANCEOF, BytecodeStream.readCPI2(code, curBCI)))) ? 1 : 0); + break; + } + case MONITORENTER: InterpreterToVM.monitorEnter(frame, nullCheck(popObject(frame, top - 1))); break; + case MONITOREXIT : InterpreterToVM.monitorExit(frame, nullCheck(popObject(frame, top - 1))); break; + + case WIDE: { + // The next opcode is never patched, plain access is fine. + int wideOpcode = BytecodeStream.opcode(code, curBCI + 1); + switch (wideOpcode) { + case ILOAD: putInt(frame, top, getLocalInt(frame, BytecodeStream.readLocalIndex2(code, curBCI))); break; + case LLOAD: putLong(frame, top, getLocalLong(frame, BytecodeStream.readLocalIndex2(code, curBCI))); break; + case FLOAD: putFloat(frame, top, getLocalFloat(frame, BytecodeStream.readLocalIndex2(code, curBCI))); break; + case DLOAD: putDouble(frame, top, getLocalDouble(frame, BytecodeStream.readLocalIndex2(code, curBCI))); break; + case ALOAD: putObject(frame, top, getLocalObject(frame, BytecodeStream.readLocalIndex2(code, curBCI))); break; + + case ISTORE: setLocalInt(frame, BytecodeStream.readLocalIndex2(code, curBCI), popInt(frame, top - 1)); break; + case LSTORE: setLocalLong(frame, BytecodeStream.readLocalIndex2(code, curBCI), popLong(frame, top - 1)); break; + case FSTORE: setLocalFloat(frame, BytecodeStream.readLocalIndex2(code, curBCI), popFloat(frame, top - 1)); break; + case DSTORE: setLocalDouble(frame, BytecodeStream.readLocalIndex2(code, curBCI), popDouble(frame, top - 1)); break; + case ASTORE: setLocalObjectOrReturnAddress(frame, BytecodeStream.readLocalIndex2(code, curBCI), popReturnAddressOrObject(frame, top - 1)); break; + case IINC: setLocalInt(frame, BytecodeStream.readLocalIndex2(code, curBCI), getLocalInt(frame, BytecodeStream.readLocalIndex2(code, curBCI)) + BytecodeStream.readIncrement2(code, curBCI)); break; + + case RET: { + top += ConstantBytecodes.stackEffectOf(RET); + curBCI = beforeJumpChecks(frame, curBCI, getLocalReturnAddress(frame, BytecodeStream.readLocalIndex2(code, curBCI)), top); + continue loop; + } + default: + throw VMError.shouldNotReachHere(Bytecodes.nameOf(curOpcode)); + } + top += Bytecodes.stackEffectOf(wideOpcode); + curBCI += (wideOpcode == IINC) ? 6 : /* wide store/load */ 4; + continue loop; + } + // @formatter:on + + case MULTIANEWARRAY: + top += allocateMultiArray(frame, top, resolveType(method, MULTIANEWARRAY, BytecodeStream.readCPI2(code, curBCI)), BytecodeStream.readUByte(code, curBCI + 3)); + break; + + default: + throw VMError.shouldNotReachHere(Bytecodes.nameOf(curOpcode)); + } + } catch (SemanticJavaException | OutOfMemoryError | StackOverflowError e) { + // Semantic Java exception thrown by interpreted code. + Throwable exception = e instanceof SemanticJavaException ? e.getCause() : e; + ExceptionHandler handler = resolveExceptionHandler(method, curBCI, exception); + if (handler != null) { + clearOperandStack(frame, method, top); + top = startingStackOffset(method.getMaxLocals()); + putObject(frame, top, exception); + top++; + curBCI = beforeJumpChecks(frame, curBCI, handler.getHandlerBCI(), top); + continue loop; + } else { + traceInterpreterException(method, indent, curBCI, top); + throw uncheckedThrow(exception); + } + } catch (Throwable e) { + // Exceptions other than SemanticJavaException (and OutOfMemoryError and + // StackoverflowError) are considered internal errors, a bug in the + // interpreter. + // VMError.shouldNotReachHere does not print the passed exception stack trace, + // so it's printed before panicking to help diagnose interpreter bugs. + e.printStackTrace(); + throw VMError.shouldNotReachHere("Unexpected host exception reached the interpreter", e); + } + + assert curOpcode != WIDE && curOpcode != LOOKUPSWITCH && curOpcode != TABLESWITCH; + + top += Bytecodes.stackEffectOf(curOpcode); + curBCI += Bytecodes.lengthOf(curOpcode); + } // loop + } + } + + @SuppressWarnings("unchecked") + private static RuntimeException uncheckedThrow(Throwable e) throws T { + throw (T) e; + } + + private static Object getReturnValueAsObject(InterpreterFrame frame, InterpreterResolvedJavaMethod method, int top) { + JavaKind returnType = method.getSignature().getReturnKind(); + // @formatter:off + return switch (returnType) { + case Boolean -> stackIntToBoolean(popInt(frame, top - 1)); + case Byte -> (byte) popInt(frame, top - 1); + case Short -> (short) popInt(frame, top - 1); + case Char -> (char) popInt(frame, top - 1); + case Int -> popInt(frame, top - 1); + case Long -> popLong(frame, top - 1); + case Float -> popFloat(frame, top - 1); + case Double -> popDouble(frame, top - 1); + case Void -> null; // void + case Object -> popObject(frame, top - 1); + default -> throw VMError.shouldNotReachHereAtRuntime(); + }; + // @formatter:on + } + + private static void clearOperandStack(InterpreterFrame frame, InterpreterResolvedJavaMethod method, int top) { + int stackStart = startingStackOffset(method.getMaxLocals()); + for (int slot = top - 1; slot >= stackStart; --slot) { + clear(frame, slot); + } + } + + private static boolean takeBranchRef1(Object operand, int opcode) { + assert IFNULL <= opcode && opcode <= IFNONNULL; + return switch (opcode) { + case IFNULL -> operand == null; + case IFNONNULL -> operand != null; + default -> throw VMError.shouldNotReachHereAtRuntime(); + }; + } + + private static boolean takeBranchPrimitive1(int operand, int opcode) { + assert IFEQ <= opcode && opcode <= IFLE; + return switch (opcode) { + case IFEQ -> operand == 0; + case IFNE -> operand != 0; + case IFLT -> operand < 0; + case IFGE -> operand >= 0; + case IFGT -> operand > 0; + case IFLE -> operand <= 0; + default -> throw VMError.shouldNotReachHereAtRuntime(); + }; + } + + private static boolean takeBranchPrimitive2(int operand1, int operand2, int opcode) { + assert IF_ICMPEQ <= opcode && opcode <= IF_ICMPLE; + return switch (opcode) { + case IF_ICMPEQ -> operand1 == operand2; + case IF_ICMPNE -> operand1 != operand2; + case IF_ICMPLT -> operand1 > operand2; + case IF_ICMPGE -> operand1 <= operand2; + case IF_ICMPGT -> operand1 < operand2; + case IF_ICMPLE -> operand1 >= operand2; + default -> throw VMError.shouldNotReachHereAtRuntime(); + }; + } + + private static boolean takeBranchRef2(Object operand1, Object operand2, int opcode) { + assert IF_ACMPEQ <= opcode && opcode <= IF_ACMPNE; + return switch (opcode) { + case IF_ACMPEQ -> operand1 == operand2; + case IF_ACMPNE -> operand1 != operand2; + default -> throw VMError.shouldNotReachHereAtRuntime(); + }; + } + + private static void arrayLoad(InterpreterFrame frame, int top, int loadOpcode) { + assert IALOAD <= loadOpcode && loadOpcode <= SALOAD; + int index = popInt(frame, top - 1); + Object array = nullCheck(popObject(frame, top - 2)); + switch (loadOpcode) { + case BALOAD -> putInt(frame, top - 2, InterpreterToVM.getArrayByte(index, array)); + case SALOAD -> putInt(frame, top - 2, InterpreterToVM.getArrayShort(index, (short[]) array)); + case CALOAD -> putInt(frame, top - 2, InterpreterToVM.getArrayChar(index, (char[]) array)); + case IALOAD -> putInt(frame, top - 2, InterpreterToVM.getArrayInt(index, (int[]) array)); + case FALOAD -> putFloat(frame, top - 2, InterpreterToVM.getArrayFloat(index, (float[]) array)); + case LALOAD -> putLong(frame, top - 2, InterpreterToVM.getArrayLong(index, (long[]) array)); + case DALOAD -> putDouble(frame, top - 2, InterpreterToVM.getArrayDouble(index, (double[]) array)); + case AALOAD -> putObject(frame, top - 2, InterpreterToVM.getArrayObject(index, (Object[]) array)); + default -> throw VMError.shouldNotReachHereAtRuntime(); + } + } + + private static void arrayStore(InterpreterFrame frame, int top, int storeOpcode) { + assert IASTORE <= storeOpcode && storeOpcode <= SASTORE; + int offset = (storeOpcode == LASTORE || storeOpcode == DASTORE) ? 2 : 1; + int index = popInt(frame, top - 1 - offset); + Object array = nullCheck(popObject(frame, top - 2 - offset)); + switch (storeOpcode) { + case BASTORE -> InterpreterToVM.setArrayByte((byte) popInt(frame, top - 1), index, array); + case SASTORE -> InterpreterToVM.setArrayShort((short) popInt(frame, top - 1), index, (short[]) array); + case CASTORE -> InterpreterToVM.setArrayChar((char) popInt(frame, top - 1), index, (char[]) array); + case IASTORE -> InterpreterToVM.setArrayInt(popInt(frame, top - 1), index, (int[]) array); + case FASTORE -> InterpreterToVM.setArrayFloat(popFloat(frame, top - 1), index, (float[]) array); + case LASTORE -> InterpreterToVM.setArrayLong(popLong(frame, top - 1), index, (long[]) array); + case DASTORE -> InterpreterToVM.setArrayDouble(popDouble(frame, top - 1), index, (double[]) array); + case AASTORE -> InterpreterToVM.setArrayObject(popObject(frame, top - 1), index, (Object[]) array); + default -> throw VMError.shouldNotReachHereAtRuntime(); + } + } + + @SuppressWarnings("unused") + private static int beforeJumpChecks(InterpreterFrame frame, int curBCI, int targetBCI, int top) { + if (targetBCI <= curBCI) { + // GR-55055: Safepoint poll needed? + } + return targetBCI; + } + + private static ExceptionHandler resolveExceptionHandler(InterpreterResolvedJavaMethod method, int bci, Throwable ex) { + ExceptionHandler[] handlers = method.getExceptionHandlers(); + ExceptionHandler resolved = null; + for (ExceptionHandler toCheck : handlers) { + if (bci >= toCheck.getStartBCI() && bci < toCheck.getEndBCI()) { + JavaType catchType = null; + if (!toCheck.isCatchAll()) { + // exception handlers are similar to instanceof bytecodes, so we pass instanceof + catchType = resolveTypeOrUnresolved(method, INSTANCEOF, (char) toCheck.catchTypeCPI()); + if (catchType instanceof UnresolvedJavaType) { + // Exception type is not reachable, skip handler. + continue; + } + } + if (catchType == null || InterpreterToVM.instanceOf(ex, (InterpreterResolvedObjectType) catchType)) { + // the first found exception handler is our exception handler + resolved = toCheck; + break; + } + } + } + return resolved; + } + + private static SemanticJavaException noClassDefFoundError(int opcode, JavaType javaType) { + String message = (javaType != null) + ? javaType.toJavaName() + : MetadataUtil.fmt("%s: (cpi = 0) unknown type", Bytecodes.nameOf(opcode)); + throw SemanticJavaException.raise(new NoClassDefFoundError(message)); + } + + private static SemanticJavaException noSuchMethodError(int opcode, JavaMethod javaMethod) { + String message = (javaMethod != null) + ? javaMethod.format("%H.%n(%P)") + : MetadataUtil.fmt("%s: (cpi = 0) unknown method", Bytecodes.nameOf(opcode)); + throw SemanticJavaException.raise(new NoSuchMethodError(message)); + } + + private static SemanticJavaException noSuchFieldError(int opcode, JavaField javaField) { + String message = (javaField != null) + ? javaField.format("%H.%n") + : MetadataUtil.fmt("%s: (cpi = 0) unknown field", Bytecodes.nameOf(opcode)); + throw SemanticJavaException.raise(new NoSuchFieldError(message)); + } + + private static void loadConstant(InterpreterFrame frame, InterpreterResolvedJavaMethod method, int top, char cpi, int opcode) { + assert opcode == LDC || opcode == LDC_W || opcode == LDC2_W; + if (GraalDirectives.injectBranchProbability(GraalDirectives.SLOWPATH_PROBABILITY, cpi == 0)) { + VMError.guarantee(opcode != LDC2_W); + throw noClassDefFoundError(opcode, null); + } + ConstantPool pool = getConstantPool(method); + Object constant = pool.lookupConstant(cpi); + if (constant instanceof PrimitiveConstant primitiveConstant) { + JavaKind kind = primitiveConstant.getJavaKind(); + assert !kind.needsTwoSlots() || opcode == LDC2_W; + assert kind.needsTwoSlots() || (opcode == LDC || opcode == LDC_W); + switch (kind) { + case Int -> putInt(frame, top, primitiveConstant.asInt()); + case Float -> putFloat(frame, top, primitiveConstant.asFloat()); + case Long -> putLong(frame, top, primitiveConstant.asLong()); + case Double -> putDouble(frame, top, primitiveConstant.asDouble()); + default -> throw VMError.shouldNotReachHereAtRuntime(); + } + } else if (constant instanceof JavaType) { + InterpreterResolvedJavaType resolvedType = resolveType(method, opcode, cpi); + putObject(frame, top, resolvedType.getJavaClass()); + } else if (constant instanceof ReferenceConstant referenceConstant) { + VMError.guarantee(referenceConstant.isNonNull(), FAILURE_CONSTANT_NOT_PART_OF_IMAGE_HEAP); + Object constantValue = referenceConstant.getReferent(); + putObject(frame, top, constantValue); + } else if (constant.equals(JavaConstant.NULL_POINTER)) { + putObject(frame, top, null); + } else { + throw VMError.unimplemented("LDC* constant pool type"); + } + } + + private static InterpreterConstantPool getConstantPool(InterpreterResolvedJavaMethod method) { + return method.getConstantPool(); + } + + private static int invoke(InterpreterFrame callerFrame, InterpreterResolvedJavaMethod method, byte[] code, int top, int curBCI, int opcode, boolean forceStayInInterpreter, + boolean preferStayInInterpreter) { + int invokeTop = top; + + char cpi = BytecodeStream.readCPI2(code, curBCI); + InterpreterResolvedJavaMethod seedMethod = Interpreter.resolveMethod(method, opcode, cpi); + + boolean hasReceiver = !seedMethod.isStatic(); + boolean isVirtual = opcode == INVOKEVIRTUAL || opcode == INVOKEINTERFACE; + + if (opcode == INVOKEDYNAMIC) { + int appendixCPI = BytecodeStream.readCPI4(code, curBCI) & 0xFFFF; + if (appendixCPI != 0) { + JavaConstant appendixConstant = method.getConstantPool().lookupAppendix(appendixCPI, opcode); + Object appendix; + if (JavaConstant.NULL_POINTER.equals(appendixConstant)) { + // The appendix is deliberately null. + appendix = null; + } else { + if (appendixConstant instanceof ReferenceConstant referenceConstant) { + appendix = referenceConstant.getReferent(); + } else { + throw VMError.shouldNotReachHere("Unexpected INVOKEDYNAMIC appendix constant: " + appendixConstant); + } + if (appendix == null) { + throw SemanticJavaException.raise(new IncompatibleClassChangeError("INVOKEDYNAMIC appendix was not included in the image heap")); + } + } + EspressoFrame.putObject(callerFrame, top, appendix); + invokeTop = top + 1; + } else { + throw VMError.shouldNotReachHere("Appendix-less INVOKEDYNAMIC"); + } + } + + InterpreterUnresolvedSignature seedSignature = seedMethod.getSignature(); + int resultAt = invokeTop - seedSignature.slotsForParameters(hasReceiver); + // The stack effect is wrt. the original top-of-the-stack. + int retStackEffect = resultAt - top; + + Object[] calleeArgs = EspressoFrame.popArguments(callerFrame, invokeTop, hasReceiver, seedSignature); + if (!seedMethod.isStatic()) { + nullCheck(calleeArgs[0]); + } + Object retObj = InterpreterToVM.dispatchInvocation(seedMethod, calleeArgs, isVirtual, forceStayInInterpreter, preferStayInInterpreter, opcode == INVOKEINTERFACE); + + retStackEffect += EspressoFrame.putKind(callerFrame, resultAt, retObj, seedSignature.getReturnKind()); + + /* instructions have fixed stack effect encoded */ + return retStackEffect - Bytecodes.stackEffectOf(opcode); + } + + // region Class/Method/Field resolution + + private static JavaType resolveTypeOrUnresolved(InterpreterResolvedJavaMethod method, int opcode, char cpi) { + assert opcode == INSTANCEOF || opcode == CHECKCAST || opcode == NEW || opcode == ANEWARRAY || opcode == MULTIANEWARRAY || opcode == LDC || opcode == LDC_W; + if (GraalDirectives.injectBranchProbability(GraalDirectives.SLOWPATH_PROBABILITY, cpi == 0)) { + throw noClassDefFoundError(opcode, null); + } + return getConstantPool(method).lookupType(cpi, opcode); + } + + private static InterpreterResolvedJavaType resolveType(InterpreterResolvedJavaMethod method, int opcode, char cpi) { + JavaType javaType = resolveTypeOrUnresolved(method, opcode, cpi); + if (GraalDirectives.injectBranchProbability(GraalDirectives.FASTPATH_PROBABILITY, javaType instanceof InterpreterResolvedJavaType)) { + return (InterpreterResolvedJavaType) javaType; + } + throw noClassDefFoundError(opcode, javaType); + } + + private static JavaMethod resolveMethodOrUnresolved(InterpreterResolvedJavaMethod method, int opcode, char cpi) { + assert Bytecodes.isInvoke(opcode); + if (GraalDirectives.injectBranchProbability(GraalDirectives.SLOWPATH_PROBABILITY, cpi == 0)) { + throw noSuchMethodError(opcode, null); + } + return getConstantPool(method).lookupMethod(cpi, opcode); + } + + static InterpreterResolvedJavaMethod resolveMethod(InterpreterResolvedJavaMethod method, int opcode, char cpi) { + JavaMethod javaMethod = resolveMethodOrUnresolved(method, opcode, cpi); + if (GraalDirectives.injectBranchProbability(GraalDirectives.FASTPATH_PROBABILITY, javaMethod instanceof InterpreterResolvedJavaMethod)) { + return (InterpreterResolvedJavaMethod) javaMethod; + } + throw noSuchMethodError(opcode, javaMethod); + } + + private static JavaField resolveFieldOrUnresolved(InterpreterResolvedJavaMethod method, int opcode, char cpi) { + assert opcode == GETFIELD || opcode == GETSTATIC || opcode == PUTFIELD || opcode == PUTSTATIC; + if (GraalDirectives.injectBranchProbability(GraalDirectives.SLOWPATH_PROBABILITY, cpi == 0)) { + throw noSuchFieldError(opcode, null); + } + return getConstantPool(method).lookupField(cpi, method, opcode); + } + + private static InterpreterResolvedJavaField resolveField(InterpreterResolvedJavaMethod method, int opcode, char cpi) { + JavaField javaField = resolveFieldOrUnresolved(method, opcode, cpi); + if (GraalDirectives.injectBranchProbability(GraalDirectives.FASTPATH_PROBABILITY, javaField instanceof InterpreterResolvedJavaField)) { + return (InterpreterResolvedJavaField) javaField; + } + throw noSuchFieldError(opcode, javaField); + } + + // endregion Class/Field/Method resolution + + private static int allocateMultiArray(InterpreterFrame frame, int top, ResolvedJavaType multiArrayType, int allocatedDimensions) { + assert multiArrayType.isArray() : multiArrayType; + assert allocatedDimensions > 0 : allocatedDimensions; + assert multiArrayType.getElementalType().getJavaKind() != JavaKind.Void; + int[] dimensions = new int[allocatedDimensions]; + for (int i = 0; i < allocatedDimensions; ++i) { + dimensions[i] = popInt(frame, top - allocatedDimensions + i); + } + Object value = InterpreterToVM.createMultiArray((InterpreterResolvedJavaType) multiArrayType, dimensions); + putObject(frame, top - allocatedDimensions, value); + return -allocatedDimensions; // Does not include the created (pushed) array. + } + + private static boolean stackIntToBoolean(int result) { + return (result & 1) != 0; + } + + // region Arithmetic/binary operations + + private static int divInt(int divisor, int dividend) { + try { + return dividend / divisor; + } catch (ArithmeticException e) { + throw SemanticJavaException.raise(e); + } + } + + private static long divLong(long divisor, long dividend) { + try { + return dividend / divisor; + } catch (ArithmeticException e) { + throw SemanticJavaException.raise(e); + } + } + + private static float divFloat(float divisor, float dividend) { + return dividend / divisor; + } + + private static double divDouble(double divisor, double dividend) { + return dividend / divisor; + } + + private static int remInt(int divisor, int dividend) { + try { + return dividend % divisor; + } catch (ArithmeticException e) { + throw SemanticJavaException.raise(e); + } + } + + private static long remLong(long divisor, long dividend) { + try { + return dividend % divisor; + } catch (ArithmeticException e) { + throw SemanticJavaException.raise(e); + } + } + + private static float remFloat(float divisor, float dividend) { + return dividend % divisor; + } + + private static double remDouble(double divisor, double dividend) { + return dividend % divisor; + } + + private static int shiftLeftInt(int bits, int value) { + return value << bits; + } + + private static long shiftLeftLong(int bits, long value) { + return value << bits; + } + + private static int shiftRightSignedInt(int bits, int value) { + return value >> bits; + } + + private static long shiftRightSignedLong(int bits, long value) { + return value >> bits; + } + + private static int shiftRightUnsignedInt(int bits, int value) { + return value >>> bits; + } + + private static long shiftRightUnsignedLong(int bits, long value) { + return value >>> bits; + } + + // endregion Arithmetic/binary operations + + // region Comparisons + + private static int compareLong(long y, long x) { + return Long.compare(x, y); + } + + private static int compareFloatGreater(float y, float x) { + return (x < y ? -1 : ((x == y) ? 0 : 1)); + } + + private static int compareFloatLess(float y, float x) { + return (x > y ? 1 : ((x == y) ? 0 : -1)); + } + + private static int compareDoubleGreater(double y, double x) { + return (x < y ? -1 : ((x == y) ? 0 : 1)); + } + + private static int compareDoubleLess(double y, double x) { + return (x > y ? 1 : ((x == y) ? 0 : -1)); + } + + // endregion Comparisons + + // region Field read/write + + /** + * Returns the offset adjustment, depending on how many slots are needed for the value that + * complete the {@link Bytecodes#stackEffectOf(int) stack effect} for the opcode. + * + *
+     *   top += putField(frame, top, resolveField(...)); break; // stack effect adjust
+     *   ...
+     *   top += Bytecodes.stackEffectOf(curOpcode);
+     *   // at this point `top` must have the correct value.
+     *   curBCI = bs.next(curBCI);
+     * 
+ */ + private static int putField(InterpreterFrame frame, int top, InterpreterResolvedJavaField field, int opcode) { + assert opcode == PUTFIELD || opcode == PUTSTATIC; + assert field.isStatic() == (opcode == PUTSTATIC); + assert !field.isUnmaterializedConstant(); + JavaKind kind = field.getJavaKind(); + assert kind != JavaKind.Illegal; + + int slotCount = kind.getSlotCount(); + Object receiver = (opcode == PUTSTATIC) + ? (kind.isPrimitive() ? StaticFieldsSupport.getStaticPrimitiveFields() : StaticFieldsSupport.getStaticObjectFields()) + : nullCheck(popObject(frame, top - slotCount - 1)); + + if (field.isStatic()) { + InterpreterToVM.ensureClassInitialized(field.getDeclaringClass()); + } + + // @formatter:off + switch (kind) { + case Boolean -> InterpreterToVM.setFieldBoolean(stackIntToBoolean(popInt(frame, top - 1)), receiver, field); + case Byte -> InterpreterToVM.setFieldByte((byte) popInt(frame, top - 1), receiver, field); + case Char -> InterpreterToVM.setFieldChar((char) popInt(frame, top - 1), receiver, field); + case Short -> InterpreterToVM.setFieldShort((short) popInt(frame, top - 1), receiver, field); + case Int -> InterpreterToVM.setFieldInt(popInt(frame, top - 1), receiver, field); + case Double -> InterpreterToVM.setFieldDouble(popDouble(frame, top - 1), receiver, field); + case Float -> InterpreterToVM.setFieldFloat(popFloat(frame, top - 1), receiver, field); + case Long -> InterpreterToVM.setFieldLong(popLong(frame, top - 1), receiver, field); + case Object -> InterpreterToVM.setFieldObject(popObject(frame, top - 1), receiver, field); + default -> throw VMError.shouldNotReachHereAtRuntime(); + } + // @formatter:on + return -slotCount + 1; + } + + /** + * Returns the offset adjustment, depending on how many slots are needed for the value that + * complete the {@link Bytecodes#stackEffectOf(int) stack effect} for the opcode. + * + *
+     *   top += getField(frame, top, resolveField(...)); break; // stack effect adjustment that depends on the field
+     *   ...
+     *   top += Bytecodes.stackEffectOf(curOpcode); // minimum stack effect
+     *   // at this point `top` must have the correct value.
+     *   curBCI = bs.next(curBCI);
+     * 
+ */ + private static int getField(InterpreterFrame frame, int top, InterpreterResolvedJavaField field, int opcode) { + assert opcode == GETFIELD || opcode == GETSTATIC; + assert field.isStatic() == (opcode == GETSTATIC); + JavaKind kind = field.getJavaKind(); + assert kind != JavaKind.Illegal; + + Object receiver = opcode == GETSTATIC + ? (kind.isPrimitive() ? StaticFieldsSupport.getStaticPrimitiveFields() : StaticFieldsSupport.getStaticObjectFields()) + : nullCheck(popObject(frame, top - 1)); + + if (field.isStatic()) { + InterpreterToVM.ensureClassInitialized(field.getDeclaringClass()); + } + + int resultAt = field.isStatic() ? top : (top - 1); + + // @formatter:off + switch (kind) { + case Boolean -> putInt(frame, resultAt, InterpreterToVM.getFieldBoolean(receiver, field) ? 1 : 0); + case Byte -> putInt(frame, resultAt, InterpreterToVM.getFieldByte(receiver, field)); + case Char -> putInt(frame, resultAt, InterpreterToVM.getFieldChar(receiver, field)); + case Short -> putInt(frame, resultAt, InterpreterToVM.getFieldShort(receiver, field)); + case Int -> putInt(frame, resultAt, InterpreterToVM.getFieldInt(receiver, field)); + case Double -> putDouble(frame, resultAt, InterpreterToVM.getFieldDouble(receiver, field)); + case Float -> putFloat(frame, resultAt, InterpreterToVM.getFieldFloat(receiver, field)); + case Long -> putLong(frame, resultAt, InterpreterToVM.getFieldLong(receiver, field)); + case Object -> putObject(frame, resultAt, InterpreterToVM.getFieldObject(receiver, field)); + default -> throw VMError.shouldNotReachHereAtRuntime(); + } + // @formatter:on + return kind.getSlotCount() - 1; + } + + // endregion Field read/write + +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectives.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectives.java new file mode 100644 index 000000000000..ca951b3a2dfc --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectives.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import jdk.vm.ci.meta.ResolvedJavaType; +import org.graalvm.nativeimage.ImageSingletons; + +public final class InterpreterDirectives { + /** + * Build-time marker to force inclusion of methods prefixed with "test" in its name + * for a given class. + * + * Only meant for testing purposes. + */ + public static void markKlass(Class targetKlass) { + ImageSingletons.lookup(InterpreterDirectivesSupport.class).markKlass(targetKlass); + } + + /** + * Tries to divert execution for the specified method Swaps the GOT entry for the specified + * method with an interpreter entry point. This requires for the given method to exist as a + * compiled method. + * + * Only meant for testing purposes. + * + * @param method This is an interpreter method obtained via + * {@link DebuggerSupport#lookupMethod(ResolvedJavaType, String, Class, Class[])}. + * + * @return True if it was possible to divert the execution for a given interpreter method. + */ + public static boolean forceInterpreterExecution(Object method) { + return ImageSingletons.lookup(InterpreterDirectivesSupport.class).forceInterpreterExecution(method); + } + + /** + * Undo operation to above. {@link #forceInterpreterExecution(Object)} must have + * happened before calling {@link #resetInterpreterExecution(Object)}. + * + * Only meant for testing purposes. + * + * @param method This is an interpreter method obtained via + * {@link DebuggerSupport#lookupMethod(ResolvedJavaType, String, Class, Class[])}. + */ + public static void resetInterpreterExecution(Object method) { + ImageSingletons.lookup(InterpreterDirectivesSupport.class).resetInterpreterExecution(method); + } + + /** + * Makes sure that the given method is executed in the interpreter, even if it doesn't have a + * dedicated compiled companion. For example, if the given method is practically inlined by all + * its callers, said callers will also be executed in the interpreter. + * + * @param method This is an interpreter method obtained via + * {@link DebuggerSupport#lookupMethod(ResolvedJavaType, String, Class, Class[])}. + * + * @return A token that can be used to undo the actions done by this operation. + */ + public static Object ensureInterpreterExecution(Object method) { + return ImageSingletons.lookup(InterpreterDirectivesSupport.class).ensureInterpreterExecution(method); + } + + /** + * Undo operation for a given token. + * + * Note: Undo operations must be applied in first-in last-out order. + * + * @param token Obtained by a call to {@link #ensureInterpreterExecution(Object)}, and defines + * which actions should be reverted. + * + * @throws IllegalStateException If an expired token is used. + */ + public static void undoExecutionOperation(Object token) { + ImageSingletons.lookup(InterpreterDirectivesSupport.class).undoExecutionOperation(token); + } + + /** + * Executes the given method in the interpreter with the provided arguments. + * + * Only meant for testing purposes. + */ + public static Object callIntoInterpreter(Object method, Object... args) { + return ImageSingletons.lookup(InterpreterDirectivesSupport.class).callIntoInterpreter(method, args); + } + + /** + * Similar to {@link #callIntoInterpreter(Object, Object...)}, but depending on the run-time + * state it either calls into the AOT compiled version or into the interpreter. + * + * Only meant for testing purposes. + */ + public static Object callIntoUnknown(Object method, Object... args) { + return ImageSingletons.lookup(InterpreterDirectivesSupport.class).callIntoUnknown(method, args); + } + + /** + * Returns true when executed in the interpreter, and false when used in compiled code. + * + * Only meant for testing purposes. + * + * Note: Be careful around its usage and folding done by the compiler. + */ + public static boolean inInterpreter() { + return true; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectivesSupport.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectivesSupport.java new file mode 100644 index 000000000000..ccbc13bcddfd --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectivesSupport.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +public interface InterpreterDirectivesSupport { + + void markKlass(Class targetKlass); + + boolean forceInterpreterExecution(Object method); + + void resetInterpreterExecution(Object method); + + Object ensureInterpreterExecution(Object method); + + void undoExecutionOperation(Object token); + + Object callIntoInterpreter(Object method, Object... args); + + Object callIntoUnknown(Object method, Object... args); +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectivesSupportImpl.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectivesSupportImpl.java new file mode 100644 index 000000000000..17f130f67e27 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterDirectivesSupportImpl.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import static com.oracle.svm.interpreter.InterpreterUtil.traceInterpreter; +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.EST_NO_ENTRY; +import static com.oracle.svm.hosted.pltgot.GOTEntryAllocator.GOT_NO_ENTRY; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.graalvm.word.Pointer; +import org.graalvm.word.UnsignedWord; +import org.graalvm.word.WordBase; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.config.ConfigurationValues; +import com.oracle.svm.core.jdk.InternalVMMethod; +import com.oracle.svm.core.meta.MethodPointer; +import com.oracle.svm.core.pltgot.GOTAccess; +import com.oracle.svm.core.pltgot.GOTHeapSupport; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; + +import jdk.graal.compiler.debug.GraalError; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +@InternalVMMethod +final class InterpreterDirectivesSupportImpl implements InterpreterDirectivesSupport { + final Map rememberCompiledEntry = new HashMap<>(); + + @Override + public boolean forceInterpreterExecution(Object method) { + InterpreterResolvedJavaMethod interpreterMethod = getInterpreterMethod(method); + + if (interpreterMethod == null) { + return false; + } + if (interpreterMethod.getGotOffset() == GOT_NO_ENTRY) { + return false; + } + if (interpreterMethod.getEnterStubOffset() == EST_NO_ENTRY) { + return false; + } + + /* arguments to Log methods might have side-effects */ + if (InterpreterOptions.InterpreterTraceSupport.getValue()) { + traceInterpreter("[forceInterpreterExecution] ").string(interpreterMethod.toString()).newline(); + } + + int estOffset = ConfigurationValues.getTarget().wordSize * interpreterMethod.getEnterStubOffset(); + Pointer estBase = InterpreterStubTable.getBaseForEnterStubTable(); + UnsignedWord estEntry = estBase.add(estOffset).readWord(0); + + WordBase previousEntry = GOTAccess.readFromGotEntry(interpreterMethod.getGotOffset()); + rememberCompiledEntry.put(interpreterMethod, previousEntry.rawValue()); + writeGOTHelper(interpreterMethod, estEntry); + + return true; + } + + private static void writeGOTHelper(InterpreterResolvedJavaMethod interpreterMethod, UnsignedWord estEntry) { + GOTHeapSupport.get().makeGOTWritable(); + GOTAccess.writeToGotEntry(interpreterMethod.getGotOffset(), estEntry); + GOTHeapSupport.get().makeGOTReadOnly(); + } + + @Override + public void resetInterpreterExecution(Object method) { + InterpreterResolvedJavaMethod interpreterMethod = getInterpreterMethod(method); + + if (interpreterMethod == null) { + // Either forceInterpreterExecution was never called or we don't have an interpreter + // method + return; + } + + if (!rememberCompiledEntry.containsKey(interpreterMethod)) { + return; + } + long previousEntry = rememberCompiledEntry.get(interpreterMethod); + writeGOTHelper(interpreterMethod, WordFactory.pointer(previousEntry)); + } + + private class InterpreterOpToken { + List changedExecutionState; + boolean valid = true; + + InterpreterOpToken() { + changedExecutionState = new ArrayList<>(); + } + } + + @Override + public Object ensureInterpreterExecution(Object method) { + InterpreterResolvedJavaMethod interpreterMethod = getInterpreterMethod(method); + InterpreterOpToken token = new InterpreterOpToken(); + + if (interpreterMethod == null) { + return null; + } + + /* arguments to Log methods might have side-effects */ + if (InterpreterOptions.InterpreterTraceSupport.getValue()) { + traceInterpreter("[ensureInterpreterExecution] ").string(interpreterMethod.toString()).newline(); + } + + for (InterpreterResolvedJavaMethod inliner : interpreterMethod.getInlinedBy()) { + if (forceInterpreterExecution(inliner)) { + token.changedExecutionState.add(0, inliner); + } + } + + if (forceInterpreterExecution(interpreterMethod)) { + token.changedExecutionState.add(0, interpreterMethod); + } + return token; + } + + @Override + public void undoExecutionOperation(Object t) { + if (t == null) { + return; + } + InterpreterOpToken token = (InterpreterOpToken) t; + if (!token.valid) { + throw new IllegalStateException("Token is expired."); + } + VMError.guarantee(token.valid); + VMError.guarantee(token.changedExecutionState != null); + for (InterpreterResolvedJavaMethod m : token.changedExecutionState) { + VMError.guarantee(m != null); + resetInterpreterExecution(m); + } + token.valid = false; + } + + @Override + public void markKlass(Class targetKlass) { + GraalError.unimplemented("should be replaced"); + } + + @Override + public Object callIntoInterpreter(Object method, Object... args) { + InterpreterResolvedJavaMethod interpreterMethod = getInterpreterMethod(method); + return Interpreter.execute(interpreterMethod, args, true); + } + + @Override + public Object callIntoUnknown(Object method, Object... args) { + InterpreterResolvedJavaMethod interpreterMethod = getInterpreterMethod(method); + MethodPointer calleeFtnPtr = interpreterMethod.getNativeEntryPoint(); + return InterpreterStubSection.leaveInterpreter(calleeFtnPtr, interpreterMethod, interpreterMethod.getDeclaringClass(), args); + } + + private static String getDescriptor(Class returnType, Class... parameterTypes) { + StringBuilder sb = new StringBuilder(); + sb.append("("); + for (Class param : parameterTypes) { + sb.append(param.descriptorString()); + } + sb.append(")"); + sb.append(returnType.descriptorString()); + return sb.toString(); + } + + private static InterpreterResolvedJavaMethod getInterpreterMethod(Object testMethod) { + VMError.guarantee(testMethod != null); + VMError.guarantee(testMethod instanceof InterpreterResolvedJavaMethod); + return (InterpreterResolvedJavaMethod) testMethod; + } + + static InterpreterResolvedJavaType getInterpreterType(Class clazz) { + VMError.guarantee(clazz != null); + + return findInterpreterResolvedJavaType(clazz); + } + + private static InterpreterResolvedJavaType findInterpreterResolvedJavaType(Class clazz) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + return (InterpreterResolvedJavaType) universe.lookupType(clazz); + } + + static InterpreterResolvedJavaMethod getInterpreterMethod(InterpreterResolvedJavaType clazz, String targetName, Class returnType, Class[] parameterTypes) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + + Collection allDeclaredMethods = universe.getAllDeclaredMethods(clazz); + + String targetDescriptor = returnType == null ? null : getDescriptor(returnType, parameterTypes); + for (ResolvedJavaMethod m : allDeclaredMethods) { + if (targetName.equals(m.getName()) && (targetDescriptor == null || targetDescriptor.equals(m.getSignature().toMethodDescriptor()))) { + return (InterpreterResolvedJavaMethod) m; + } + } + + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterFeature.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterFeature.java new file mode 100644 index 000000000000..85251cef3364 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterFeature.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import static jdk.graal.compiler.nodeinfo.NodeCycles.CYCLES_0; +import static jdk.graal.compiler.nodeinfo.NodeSize.SIZE_0; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; + +import org.graalvm.nativeimage.AnnotationAccess; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.function.CEntryPoint; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.word.Pointer; +import org.graalvm.word.WordBase; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.core.ParsingReason; +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.interpreter.InterpreterSupport; +import com.oracle.svm.core.meta.MethodPointer; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.core.graal.code.InterpreterAccessStubData; +import com.oracle.svm.core.graal.aarch64.AArch64InterpreterStubs; +import com.oracle.svm.core.graal.amd64.AMD64InterpreterStubs; +import com.oracle.svm.interpreter.debug.DebuggerEventsFeature; +import com.oracle.svm.graal.hosted.DeoptimizationFeature; +import com.oracle.svm.hosted.FeatureImpl; +import com.oracle.svm.hosted.code.SubstrateCompilationDirectives; +import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.util.ReflectionUtil; + +import jdk.graal.compiler.api.replacements.Fold; +import jdk.graal.compiler.core.common.type.StampFactory; +import jdk.graal.compiler.graph.NodeClass; +import jdk.graal.compiler.nodeinfo.NodeInfo; +import jdk.graal.compiler.nodes.ConstantNode; +import jdk.graal.compiler.nodes.calc.FloatingNode; +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration; +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderContext; +import jdk.graal.compiler.nodes.graphbuilderconf.InvocationPlugin; +import jdk.graal.compiler.nodes.graphbuilderconf.InvocationPlugins.Registration; +import jdk.graal.compiler.nodes.spi.Lowerable; +import jdk.graal.compiler.nodes.spi.LoweringTool; +import jdk.graal.compiler.phases.util.Providers; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.Local; +import jdk.vm.ci.meta.LocalVariableTable; +import jdk.vm.ci.meta.MetaAccessProvider; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.Signature; + +@Platforms(Platform.HOSTED_ONLY.class) +public class InterpreterFeature implements InternalFeature { + private AnalysisMethod leaveStub; + + static boolean executableByInterpreter(AnalysisMethod m) { + if (AnnotationAccess.getAnnotation(m, CFunction.class) != null) { + return false; + } + if (AnnotationAccess.getAnnotation(m, CEntryPoint.class) != null) { + return false; + } + Uninterruptible uninterruptible = AnnotationAccess.getAnnotation(m, Uninterruptible.class); + if (uninterruptible != null) { + if (uninterruptible.mayBeInlined() && !uninterruptible.callerMustBe()) { + /* + * this method was only annotated with @Uninterruptible so that it can be called + * from uninterruptible code, not because it has to be uninterruptible itself. + */ + } else { + return false; + } + } + + return true; + } + + public static boolean callableByInterpreter(ResolvedJavaMethod m, MetaAccessProvider metaAccess) { + if (AnnotationAccess.getAnnotation(m, Fold.class) != null) { + /* + * GR-55052: For now @Fold methods are considered not callable. The problem is that such + * methods are reachability cut-offs, so we would need to roll our own reachability + * analysis to have all the relevant methods available at run-time. Instead we should + * replace the call-site with the constant according to SVM semantics. + */ + return false; + } + + ResolvedJavaType wordBaseType = metaAccess.lookupJavaType(WordBase.class); + + if (wordBaseType.isAssignableFrom(m.getDeclaringClass())) { + return false; + } + + Signature signature = m.getSignature(); + if (wordBaseType.isAssignableFrom(signature.getReturnType(m.getDeclaringClass()).resolve(m.getDeclaringClass()))) { + return false; + } + + for (int i = 0; i < signature.getParameterCount(false); i++) { + if (wordBaseType.isAssignableFrom(signature.getParameterType(i, null).resolve(m.getDeclaringClass()))) { + return false; + } + } + + return true; + } + + @Override + public List> getRequiredFeatures() { + return Arrays.asList( + DeoptimizationFeature.class, + DebuggerEventsFeature.class); + } + + @Override + public void registerInvocationPlugins(Providers providers, GraphBuilderConfiguration.Plugins plugins, ParsingReason reason) { + Registration r = new Registration(plugins.getInvocationPlugins(), InterpreterDirectives.class); + + r.register(new InvocationPlugin.RequiredInvocationPlugin("inInterpreter") { + @Override + public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver) { + /* Dummy node so that analysis will not be able to "see through it". */ + b.addPush(JavaKind.Boolean, new InInterpreterNode()); + return true; + } + }); + } + + @NodeInfo(cycles = CYCLES_0, size = SIZE_0) + public static final class InInterpreterNode extends FloatingNode implements Lowerable { + public static final NodeClass TYPE = NodeClass.create(InInterpreterNode.class); + + public InInterpreterNode() { + super(TYPE, StampFactory.forKind(JavaKind.Boolean)); + } + + @Override + public void lower(LoweringTool tool) { + replaceAtUsagesAndDelete(graph().unique(ConstantNode.forBoolean(false))); + } + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + if (Platform.includedIn(Platform.AARCH64.class)) { + ImageSingletons.add(InterpreterStubSection.class, new AArch64InterpreterStubSection()); + ImageSingletons.add(InterpreterAccessStubData.class, new AArch64InterpreterStubs.AArch64InterpreterAccessStubData()); + } else if (Platform.includedIn(Platform.AMD64.class)) { + ImageSingletons.add(InterpreterStubSection.class, new AMD64InterpreterStubSection()); + ImageSingletons.add(InterpreterAccessStubData.class, new AMD64InterpreterStubs.AMD64InterpreterAccessStubData()); + } else { + VMError.unsupportedFeature("Platform not supported yet"); + } + } + + private static int findLocalSlotByName(String localName, Local[] locals) { + for (Local local : locals) { + if (localName.equals(local.getName())) { + return local.getSlot(); + } + } + throw new NoSuchElementException(localName); + } + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + FeatureImpl.BeforeAnalysisAccessImpl accessImpl = (FeatureImpl.BeforeAnalysisAccessImpl) access; + + BuildTimeInterpreterUniverse.freshSingletonInstance(); + AnalysisMethod interpreterRoot = accessImpl.getMetaAccess().lookupJavaType(Interpreter.Root.class).getDeclaredMethods(false)[0]; + + accessImpl.registerAsRoot(interpreterRoot, true, "interpreter main loop"); + LocalVariableTable interpreterVariableTable = interpreterRoot.getLocalVariableTable(); + int interpretedMethodSlot = findLocalSlotByName("method", interpreterVariableTable.getLocalsAt(0)); // parameter + int interpreterFrameSlot = findLocalSlotByName("frame", interpreterVariableTable.getLocalsAt(0)); // parameter + // Local variable, search all locals. + int bciSlot = findLocalSlotByName("curBCI", interpreterVariableTable.getLocals()); + + ImageSingletons.add(InterpreterSupport.class, new InterpreterSupportImpl(bciSlot, interpretedMethodSlot, interpreterFrameSlot)); + ImageSingletons.add(InterpreterDirectivesSupport.class, new InterpreterDirectivesSupportImpl()); + ImageSingletons.add(InterpreterMethodPointerHolder.class, new InterpreterMethodPointerHolder()); + + // Locals must be available at runtime to retrieve BCI, interpreted method and interpreter + // frame. + SubstrateCompilationDirectives.singleton().registerFrameInformationRequired(interpreterRoot); + + Method leaveMethod = ReflectionUtil.lookupMethod(InterpreterStubSection.class, "leaveInterpreterStub", CFunctionPointer.class, Pointer.class, long.class, long.class); + leaveStub = accessImpl.getMetaAccess().lookupJavaMethod(leaveMethod); + accessImpl.registerAsRoot(leaveStub, true, "low level entry point"); + } + + @Override + public void beforeCompilation(BeforeCompilationAccess access) { + FeatureImpl.BeforeCompilationAccessImpl accessImpl = (FeatureImpl.BeforeCompilationAccessImpl) access; + + /* required so that it can hold a relocatable pointer */ + accessImpl.registerAsImmutable(InterpreterMethodPointerHolder.singleton()); + accessImpl.registerAsImmutable(InterpreterSupport.singleton()); + } + + @Override + public void afterCompilation(AfterCompilationAccess access) { + FeatureImpl.AfterCompilationAccessImpl accessImpl = (FeatureImpl.AfterCompilationAccessImpl) access; + + HostedMethod hLeaveStub = accessImpl.getUniverse().lookup(leaveStub); + int leaveStubLength = accessImpl.getCompilations().get(hLeaveStub).result.getTargetCodeSize(); + + InterpreterSupport.setLeaveStubPointer(new MethodPointer(hLeaveStub), leaveStubLength); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterFrame.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterFrame.java new file mode 100644 index 000000000000..37af8235dd29 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterFrame.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import java.util.Arrays; + +import com.oracle.svm.core.monitor.MonitorSupport; +import com.oracle.svm.core.util.VMError; + +public final class InterpreterFrame { + private final long[] primitives; + private final Object[] references; + + private final Object[] arguments; + private Object[] locks; + private int lockCount; + + private static final Object[] EMPTY = new Object[0]; + + private InterpreterFrame(int slotCount, Object[] arguments) { + this.primitives = new long[slotCount]; + this.references = new Object[slotCount]; + this.arguments = arguments; + this.lockCount = 0; + this.locks = EMPTY; + } + + static InterpreterFrame create(int slotCount, Object... arguments) { + return new InterpreterFrame(slotCount, arguments); + } + + Object[] getArguments() { + return arguments; + } + + int getIntStatic(int slot) { + return (int) primitives[slot]; + } + + Object getObjectStatic(int slot) { + return references[slot]; + } + + float getFloatStatic(int slot) { + return Float.intBitsToFloat((int) primitives[slot]); + } + + long getLongStatic(int slot) { + return primitives[slot]; + } + + double getDoubleStatic(int slot) { + return Double.longBitsToDouble(primitives[slot]); + } + + void setObjectStatic(int slot, Object value) { + references[slot] = value; + } + + void setIntStatic(int slot, int value) { + primitives[slot] = value; + } + + void setFloatStatic(int slot, float value) { + primitives[slot] = Float.floatToRawIntBits(value); + } + + void setLongStatic(int slot, long value) { + primitives[slot] = value; + } + + void setDoubleStatic(int slot, double value) { + primitives[slot] = Double.doubleToRawLongBits(value); + } + + void clearObjectStatic(int slot) { + references[slot] = null; + } + + void clearPrimitiveStatic(int slot) { + primitives[slot] = 0; + } + + void clearStatic(int slot) { + clearObjectStatic(slot); + clearPrimitiveStatic(slot); + } + + void swapStatic(int src, int dst) { + long tmp = primitives[src]; + primitives[src] = primitives[dst]; + primitives[dst] = tmp; + + Object otmp = references[src]; + references[src] = references[dst]; + references[dst] = otmp; + } + + void copyStatic(int src, int dst) { + primitives[dst] = primitives[src]; + references[dst] = references[src]; + } + + private void ensureLocksCapacity(int capacity) { + int oldLength = locks.length; + Object[] newLocks = Arrays.copyOf(locks, Math.max(capacity, (oldLength * 2) + 1)); + this.locks = newLocks; + } + + void addLock(Object ref) { + assert ref != null; + assert MonitorSupport.singleton().isLockedByCurrentThread(ref); + if (lockCount >= 0) { + // Fast path, balanced locks. + if (lockCount >= locks.length) { + ensureLocksCapacity(lockCount + 1); + } + locks[lockCount++] = ref; + } else { + // Unbalanced locks, linear scan. + for (int i = 0; i < locks.length; ++i) { + if (locks[i] == null) { + locks[i] = ref; + return; + } + } + // No free slot found. + int oldLockCount = locks.length; + ensureLocksCapacity(oldLockCount + 1); + assert locks[oldLockCount] == null; + locks[oldLockCount] = ref; + } + } + + void removeLock(Object ref) { + assert ref != null; + if (lockCount > 0 && locks[lockCount - 1] == ref) { + // Fast path, balanced locks. + locks[--lockCount] = null; + } else { + lockCount = -1; + // Unbalanced locks, linear scan. + for (int i = 0; i < locks.length; ++i) { + if (locks[i] == ref) { + locks[i] = null; + return; + } + } + throw VMError.shouldNotReachHere("lock not found in interpreter frame"); + } + } + + Object[] getLocks() { + return locks; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterMethodPointerHolder.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterMethodPointerHolder.java new file mode 100644 index 000000000000..3ea10df4ef1e --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterMethodPointerHolder.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import com.oracle.svm.core.BuildPhaseProvider; +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.heap.UnknownPrimitiveField; + +import jdk.graal.compiler.api.replacements.Fold; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.function.CFunctionPointer; + +public class InterpreterMethodPointerHolder { + @Fold + public static InterpreterMethodPointerHolder singleton() { + return ImageSingletons.lookup(InterpreterMethodPointerHolder.class); + } + + @UnknownPrimitiveField(availability = BuildPhaseProvider.AfterCompilation.class) private CFunctionPointer methodNotCompiledFtnPtr; + + @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) + public static CFunctionPointer getMethodNotCompiledHandler() { + CFunctionPointer ptr = singleton().methodNotCompiledFtnPtr; + assert ptr.rawValue() != 0; + return ptr; + } + + @Platforms(Platform.HOSTED_ONLY.class) + public static void setMethodNotCompiledHandler(CFunctionPointer ftnptr) { + assert singleton().methodNotCompiledFtnPtr == null; + singleton().methodNotCompiledFtnPtr = ftnptr; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterOptions.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterOptions.java new file mode 100644 index 000000000000..6fb516d7f80f --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterOptions.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import org.graalvm.collections.EconomicMap; + +import com.oracle.svm.core.hub.RuntimeClassLoading; +import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.core.option.RuntimeOptionKey; +import com.oracle.svm.hosted.pltgot.PLTGOTOptions; + +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionKey; +import jdk.graal.compiler.options.OptionType; + +public class InterpreterOptions { + @Option(help = "Adds support to divert execution from AOT compiled methods to the interpreter at run-time.", type = OptionType.Expert) // + public static final HostedOptionKey DebuggerWithInterpreter = new HostedOptionKey<>(false) { + @Override + protected void onValueUpdate(EconomicMap, Object> values, Boolean oldValue, Boolean newValue) { + super.onValueUpdate(values, oldValue, newValue); + if (newValue) { + PLTGOTOptions.EnablePLTGOT.update(values, true); + } + } + }; + + @Option(help = "Enables logging at build time related to the interpreter setup", type = OptionType.Expert)// + public static final HostedOptionKey InterpreterBuildTimeLogging = new HostedOptionKey<>(false); + + @Option(help = "Path to dump interpreter universe metadata as .class files", type = OptionType.Expert)// + public static final HostedOptionKey InterpreterDumpClassFiles = new HostedOptionKey<>(""); + + // GR-54939: Switch default to false, as this has roughly a 2x perf impact on interpreter + // performance. + @Option(help = "Include interpreter tracing code in image") public static final HostedOptionKey InterpreterTraceSupport = new HostedOptionKey<>(true); + + @Option(help = "Trace Interpreter execution")// + public static final RuntimeOptionKey InterpreterTrace = new RuntimeOptionKey<>(false); + + public static boolean interpreterEnabled() { + return DebuggerWithInterpreter.getValue() || RuntimeClassLoading.isSupported(); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterStubSection.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterStubSection.java new file mode 100644 index 000000000000..b64e99e037e3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterStubSection.java @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter; + +import com.oracle.objectfile.BasicProgbitsSectionImpl; +import com.oracle.objectfile.ObjectFile; +import com.oracle.objectfile.SectionName; +import com.oracle.svm.core.NeverInline; +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.SubstrateTargetDescription; +import com.oracle.svm.core.config.ConfigurationValues; +import com.oracle.svm.core.deopt.Deoptimizer; +import com.oracle.svm.core.graal.meta.KnownOffsets; +import com.oracle.svm.core.graal.code.InterpreterAccessStubData; +import com.oracle.svm.core.graal.code.SubstrateCallingConventionKind; +import com.oracle.svm.core.graal.code.SubstrateCallingConventionType; +import com.oracle.svm.core.jdk.InternalVMMethod; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.image.AbstractImage; +import com.oracle.svm.hosted.image.NativeImage; +import com.oracle.svm.hosted.image.RelocatableBuffer; +import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterUnresolvedSignature; +import jdk.graal.compiler.core.common.LIRKind; +import jdk.graal.compiler.core.common.NumUtil; +import jdk.graal.compiler.word.Word; +import jdk.vm.ci.code.CallingConvention; +import jdk.vm.ci.code.RegisterConfig; +import jdk.vm.ci.code.ValueKindFactory; +import jdk.vm.ci.meta.AllocatableValue; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.nativeimage.impl.UnmanagedMemorySupport; +import org.graalvm.word.Pointer; +import org.graalvm.word.WordFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static com.oracle.svm.interpreter.EspressoFrame.setLocalDouble; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalFloat; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalInt; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalLong; +import static com.oracle.svm.interpreter.EspressoFrame.setLocalObject; + +@InternalVMMethod +public abstract class InterpreterStubSection { + public static final SectionName SVM_INTERP = new SectionName.ProgbitsSectionName("svm_interp"); + + protected RegisterConfig registerConfig; + protected SubstrateTargetDescription target; + protected ValueKindFactory valueKindFactory; + + private ObjectFile.ProgbitsSectionImpl stubsBufferImpl; + + private final Map enterTrampolineOffsets = new HashMap<>(); + + public void createInterpreterEnterStubSection(AbstractImage image, Collection methods) { + ObjectFile objectFile = image.getObjectFile(); + byte[] stubsBlob = generateEnterStubs(methods); + + RelocatableBuffer stubsBuffer = new RelocatableBuffer(stubsBlob.length, objectFile.getByteOrder()); + stubsBufferImpl = new BasicProgbitsSectionImpl(stubsBuffer.getBackingArray()); + ObjectFile.Section stubsSection = objectFile.newProgbitsSection(SVM_INTERP.getFormatDependentName(objectFile.getFormat()), objectFile.getPageSize(), false, true, stubsBufferImpl); + + stubsBuffer.getByteBuffer().put(stubsBlob, 0, stubsBlob.length); + + boolean internalSymbolsAreGlobal = SubstrateOptions.InternalSymbolsAreGlobal.getValue(); + objectFile.createDefinedSymbol("interp_enter_trampoline", stubsSection, 0, 0, true, internalSymbolsAreGlobal); + + for (InterpreterResolvedJavaMethod method : enterTrampolineOffsets.keySet()) { + int offset = enterTrampolineOffsets.get(method); + objectFile.createDefinedSymbol(nameForInterpMethod(method), stubsSection, offset, ConfigurationValues.getTarget().wordSize, true, internalSymbolsAreGlobal); + } + } + + public static String nameForInterpMethod(InterpreterResolvedJavaMethod method) { + return "interp_enter_" + NativeImage.localSymbolNameForMethod(method); + } + + public void createInterpreterVtableEnterStubSection(AbstractImage image, int maxVtableIndex) { + ObjectFile objectFile = image.getObjectFile(); + byte[] stubsBlob = generateVtableEnterStubs(maxVtableIndex); + + RelocatableBuffer stubsBuffer = new RelocatableBuffer(stubsBlob.length, objectFile.getByteOrder()); + stubsBufferImpl = new BasicProgbitsSectionImpl(stubsBuffer.getBackingArray()); + + // TODO: if the section should be re-used, we need to respect the offsets into this section. + // or just a new dedicated section? + ObjectFile.Section stubsSection = objectFile.newProgbitsSection(SVM_INTERP.getFormatDependentName(objectFile.getFormat()), objectFile.getPageSize(), false, true, stubsBufferImpl); + + stubsBuffer.getByteBuffer().put(stubsBlob, 0, stubsBlob.length); + + boolean internalSymbolsAreGlobal = SubstrateOptions.InternalSymbolsAreGlobal.getValue(); + objectFile.createDefinedSymbol("crema_enter_trampoline", stubsSection, 0, 0, true, internalSymbolsAreGlobal); + + int vtableEntrySize = KnownOffsets.singleton().getVTableEntrySize(); + for (int index = 0; index < maxVtableIndex; index++) { + int offset = index * vtableEntrySize; + objectFile.createDefinedSymbol(nameForVtableOffset(offset), stubsSection, offset, ConfigurationValues.getTarget().wordSize, true, internalSymbolsAreGlobal); + } + } + + public static String nameForVtableOffset(int offset) { + return "crema_enter_" + String.format("%04x", offset); + } + + protected void recordEnterTrampoline(InterpreterResolvedJavaMethod m, int position) { + enterTrampolineOffsets.put(m, position); + } + + public abstract int getVTableStubSize(); + + protected abstract byte[] generateEnterStubs(Collection methods); + + protected abstract byte[] generateVtableEnterStubs(int maxVtableIndex); + + public void markEnterStubPatch(HostedMethod enterStub) { + markEnterStubPatch(stubsBufferImpl, enterStub); + } + + protected abstract void markEnterStubPatch(ObjectFile.ProgbitsSectionImpl pltBuffer, ResolvedJavaMethod enterStub); + + @Deoptimizer.DeoptStub(stubType = Deoptimizer.StubType.InterpreterEnterStub) + @NeverInline("needs ABI boundary") + public static Pointer enterInterpreterStub(int interpreterMethodESTOffset, Pointer enterData) { + InterpreterAccessStubData accessHelper = ImageSingletons.lookup(InterpreterAccessStubData.class); + InterpreterStubSection stubSection = ImageSingletons.lookup(InterpreterStubSection.class); + DebuggerSupport interpreterSupport = ImageSingletons.lookup(DebuggerSupport.class); + + InterpreterResolvedJavaMethod interpreterMethod = (InterpreterResolvedJavaMethod) interpreterSupport.getUniverse().getMethodForESTOffset(interpreterMethodESTOffset); + VMError.guarantee(interpreterMethod != null); + + InterpreterUnresolvedSignature signature = interpreterMethod.getSignature(); + VMError.guarantee(signature != null); + + int count = signature.getParameterCount(false); + + ResolvedJavaType accessingClass = interpreterMethod.getDeclaringClass(); + + JavaType thisType = interpreterMethod.hasReceiver() ? accessingClass : null; + JavaType returnType = signature.getReturnType(accessingClass); + + var kind = SubstrateCallingConventionKind.Java.toType(true); + CallingConvention callingConvention = stubSection.registerConfig.getCallingConvention(kind, returnType, signature.toParameterTypes(thisType), stubSection.valueKindFactory); + + InterpreterFrame frame = EspressoFrame.allocate(interpreterMethod.getMaxLocals(), interpreterMethod.getMaxStackSize(), new Object[0]); + + int interpSlot = 0; + int gpIdx = 0; + int fpIdx = 0; + if (interpreterMethod.hasReceiver()) { + Object receiver = ((Pointer) WordFactory.pointer(accessHelper.getGpArgumentAt(callingConvention.getArgument(gpIdx), enterData, gpIdx))).toObject(); + setLocalObject(frame, 0, receiver); + gpIdx++; + interpSlot++; + } + + for (int i = 0; i < count; i++) { + JavaKind argKind = signature.getParameterKind(i); + long arg; + AllocatableValue ccArg = callingConvention.getArgument(gpIdx + fpIdx); + switch (argKind) { + case Float: + case Double: + arg = accessHelper.getFpArgumentAt(ccArg, enterData, fpIdx); + fpIdx++; + break; + default: + arg = accessHelper.getGpArgumentAt(ccArg, enterData, gpIdx); + gpIdx++; + break; + } + + switch (argKind) { + // @formatter:off + case Boolean: setLocalInt(frame, interpSlot, (arg & 0xff) != 0 ? 1 : 0); break; + case Byte: setLocalInt(frame, interpSlot, (byte) arg); break; + case Short: setLocalInt(frame, interpSlot, (short) arg); break; + case Char: setLocalInt(frame, interpSlot, (char) arg); break; + case Int: setLocalInt(frame, interpSlot, (int) arg); break; + case Long: setLocalLong(frame, interpSlot, arg); interpSlot++; break; + case Float: setLocalFloat(frame, interpSlot, Float.intBitsToFloat((int) arg)); break; + case Double: setLocalDouble(frame, interpSlot, Double.longBitsToDouble(arg)); interpSlot++; break; + case Object: setLocalObject(frame, interpSlot, ((Pointer) WordFactory.pointer(arg)).toObject()); break; + // @formatter:on + default: + throw VMError.shouldNotReachHereAtRuntime(); + } + interpSlot++; + } + + Object retVal = Interpreter.execute(interpreterMethod, frame); + + switch (returnType.getJavaKind()) { + case Boolean: + assert retVal instanceof Boolean; + accessHelper.setGpReturn(enterData, ((Boolean) retVal) ? 1 : 0); + break; + case Byte: + assert retVal instanceof Byte; + accessHelper.setGpReturn(enterData, ((Byte) retVal).longValue()); + break; + case Short: + assert retVal instanceof Short; + accessHelper.setGpReturn(enterData, ((Short) retVal).longValue()); + break; + case Char: + assert retVal instanceof Character; + accessHelper.setGpReturn(enterData, ((Character) retVal).charValue()); + break; + case Int: + assert retVal instanceof Integer; + accessHelper.setGpReturn(enterData, ((Integer) retVal).longValue()); + break; + case Long: + assert retVal instanceof Long; + accessHelper.setGpReturn(enterData, (Long) retVal); + break; + case Float: + assert retVal instanceof Float; + accessHelper.setFpReturn(enterData, Float.floatToRawIntBits((float) retVal)); + break; + case Double: + assert retVal instanceof Double; + accessHelper.setFpReturn(enterData, Double.doubleToRawLongBits((double) retVal)); + break; + case Object: + accessHelper.setGpReturn(enterData, Word.objectToTrackedPointer(retVal).rawValue()); + break; + case Void: + break; + default: + throw VMError.shouldNotReachHereAtRuntime(); + } + return enterData; + } + + /* + * reserve four slots for: 1. base address of outgoing stack args, 2. variable stack size, 3. + * gcReferenceMap, 4. stack padding to match alignment + */ + @Deoptimizer.DeoptStub(stubType = Deoptimizer.StubType.InterpreterLeaveStub) + @NeverInline("needs ABI boundary") + @SuppressWarnings("unused") + public static Pointer leaveInterpreterStub(CFunctionPointer entryPoint, Pointer leaveData, long stackSize, long gcReferenceMap) { + return (Pointer) entryPoint; + } + + public static Object leaveInterpreter(CFunctionPointer compiledEntryPoint, InterpreterResolvedJavaMethod seedMethod, ResolvedJavaType accessingClass, Object[] args) { + InterpreterUnresolvedSignature targetSignature = seedMethod.getSignature(); + InterpreterStubSection stubSection = ImageSingletons.lookup(InterpreterStubSection.class); + + JavaType thisType = seedMethod.hasReceiver() ? seedMethod.getDeclaringClass() : null; + SubstrateCallingConventionType kind = SubstrateCallingConventionKind.Java.toType(true); + JavaType returnType = targetSignature.getReturnType(accessingClass); + + CallingConvention callingConvention = stubSection.registerConfig.getCallingConvention(kind, returnType, targetSignature.toParameterTypes(thisType), stubSection.valueKindFactory); + + InterpreterAccessStubData accessHelper = ImageSingletons.lookup(InterpreterAccessStubData.class); + Pointer leaveData = StackValue.get(1, accessHelper.allocateStubDataSize()); + + /* GR-54726: Reference map is currently limited to 64 arguments */ + long gcReferenceMap = 0; + int gpIdx = 0; + int fpIdx = 0; + if (seedMethod.hasReceiver()) { + gcReferenceMap |= accessHelper.setGpArgumentAt(callingConvention.getArgument(gpIdx), leaveData, gpIdx, Word.objectToTrackedPointer(args[0]).rawValue()); + gpIdx++; + } + + int stackSize = NumUtil.roundUp(callingConvention.getStackSize(), stubSection.target.stackAlignment); + + Pointer stackBuffer = WordFactory.nullPointer(); + if (stackSize > 0) { + stackBuffer = ImageSingletons.lookup(UnmanagedMemorySupport.class).malloc(WordFactory.unsigned(stackSize)); + accessHelper.setSp(leaveData, stackSize, stackBuffer); + } + + int argCount = targetSignature.getParameterCount(false); + for (int i = 0; i < argCount; i++) { + Object arg = args[i + (seedMethod.hasReceiver() ? 1 : 0)]; + + AllocatableValue ccArg = callingConvention.getArgument(gpIdx + fpIdx); + JavaType type = targetSignature.getParameterType(i, accessingClass); + switch (type.getJavaKind()) { + case Boolean: + accessHelper.setGpArgumentAt(ccArg, leaveData, gpIdx, (boolean) arg ? 1 : 0); + gpIdx++; + break; + case Byte: + accessHelper.setGpArgumentAt(ccArg, leaveData, gpIdx, (byte) arg); + gpIdx++; + break; + case Short: + accessHelper.setGpArgumentAt(ccArg, leaveData, gpIdx, (short) arg); + gpIdx++; + break; + case Char: + accessHelper.setGpArgumentAt(ccArg, leaveData, gpIdx, (char) arg); + gpIdx++; + break; + case Int: + accessHelper.setGpArgumentAt(ccArg, leaveData, gpIdx, (int) arg); + gpIdx++; + break; + case Long: + accessHelper.setGpArgumentAt(ccArg, leaveData, gpIdx, (long) arg); + gpIdx++; + break; + case Object: + gcReferenceMap |= accessHelper.setGpArgumentAt(ccArg, leaveData, gpIdx, Word.objectToTrackedPointer(arg).rawValue()); + gpIdx++; + break; + + case Float: + accessHelper.setFpArgumentAt(ccArg, leaveData, fpIdx, Float.floatToRawIntBits((float) arg)); + fpIdx++; + break; + case Double: + accessHelper.setFpArgumentAt(ccArg, leaveData, fpIdx, Double.doubleToRawLongBits((double) arg)); + fpIdx++; + break; + + default: + throw VMError.shouldNotReachHereAtRuntime(); + } + } + + VMError.guarantee(compiledEntryPoint.isNonNull()); + + try { + // GR-55022: Stack overflow check should be done here + leaveInterpreterStub(compiledEntryPoint, leaveData, stackSize, gcReferenceMap); + } catch (Throwable e) { + // native code threw exception, wrap it + throw SemanticJavaException.raise(e); + } finally { + if (stackSize > 0) { + VMError.guarantee(stackBuffer.isNonNull()); + ImageSingletons.lookup(UnmanagedMemorySupport.class).free(stackBuffer); + } + } + + // @formatter:off + return switch (returnType.getJavaKind()) { + case Boolean -> (accessHelper.getGpReturn(leaveData) & 0xff) != 0; + case Byte -> (byte) accessHelper.getGpReturn(leaveData); + case Short -> (short) accessHelper.getGpReturn(leaveData); + case Char -> (char) accessHelper.getGpReturn(leaveData); + case Int -> (int) accessHelper.getGpReturn(leaveData); + case Long -> accessHelper.getGpReturn(leaveData); + case Float -> Float.intBitsToFloat((int) accessHelper.getFpReturn(leaveData)); + case Double -> Double.longBitsToDouble(accessHelper.getFpReturn(leaveData)); + case Object -> ((Pointer) WordFactory.pointer(accessHelper.getGpReturn(leaveData))).toObject(); + case Void -> null; + default -> throw VMError.shouldNotReachHereAtRuntime(); + }; + // @formatter:on + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterStubTable.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterStubTable.java new file mode 100644 index 000000000000..bd70e0f95f63 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterStubTable.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import com.oracle.objectfile.BasicProgbitsSectionImpl; +import com.oracle.objectfile.ObjectFile; +import com.oracle.objectfile.SectionName; +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.c.CGlobalData; +import com.oracle.svm.core.c.CGlobalDataFactory; +import com.oracle.svm.core.config.ConfigurationValues; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.image.AbstractImage; +import com.oracle.svm.hosted.image.RelocatableBuffer; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import org.graalvm.word.Pointer; + +import java.nio.ByteBuffer; +import java.util.Collection; + +public class InterpreterStubTable { + final SectionName section; + private RelocatableBuffer tableBuffer; + private ObjectFile.ProgbitsSectionImpl tableBufferImpl; + private int offsetHash; + + public InterpreterStubTable() { + String sectionName = "s_intrp_ent"; + VMError.guarantee(sectionName.length() <= 16, "mach-O limitation"); + this.section = new SectionName.ProgbitsSectionName(sectionName); + } + + protected void installAdditionalInfoIntoImageObjectFile(AbstractImage image, Collection methods) { + ObjectFile objectFile = image.getObjectFile(); + + int wordSize = ConfigurationValues.getTarget().wordSize; + int hashSize = 4 /* size */ + 6 /* "crc32:" */ + 8 /* actual hash */; + assert hashSize == 18; + int size = methods.size() * wordSize + hashSize; + offsetHash = size - hashSize; + + tableBuffer = new RelocatableBuffer(size, objectFile.getByteOrder()); + tableBufferImpl = new BasicProgbitsSectionImpl(tableBuffer.getBackingArray()); + ObjectFile.Section tableSection = objectFile.newProgbitsSection(section.getFormatDependentName(objectFile.getFormat()), objectFile.getPageSize(), true, false, tableBufferImpl); + + objectFile.createDefinedSymbol(SYMBOL_NAME, tableSection, 0, 0, false, SubstrateOptions.InternalSymbolsAreGlobal.getValue()); + + // Store an additional blob of bytes to verify the interpreter metadata integrity. + objectFile.createDefinedSymbol(DebuggerSupport.IMAGE_INTERP_HASH_SYMBOL_NAME, tableSection, offsetHash, 0, true, true); + + ObjectFile.RelocationKind relocationKind = ObjectFile.RelocationKind.getDirect(wordSize); + for (InterpreterResolvedJavaMethod method : methods) { + int offset = wordSize * method.getEnterStubOffset(); + assert offset < size; + tableBufferImpl.markRelocationSite(offset, relocationKind, InterpreterStubSection.nameForInterpMethod(method), 0L); + } + } + + protected void writeMetadataHashString(byte[] metadataHash) { + assert metadataHash.length == (6 + 8) : metadataHash.length; + ByteBuffer bb = tableBuffer.getByteBuffer(); + bb.position(offsetHash); + bb.putInt(metadataHash.length); + bb.put(metadataHash); + } + + private static final String SYMBOL_NAME = "__svm_interp_enter_table"; + + private static final CGlobalData BASE = CGlobalDataFactory. forSymbol(SYMBOL_NAME); + + public static Pointer getBaseForEnterStubTable() { + return BASE.get(); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterSupportImpl.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterSupportImpl.java new file mode 100644 index 000000000000..6eeee427e546 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterSupportImpl.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter; + +import com.oracle.svm.core.interpreter.InterpreterFrameSourceInfo; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.word.Pointer; +import org.graalvm.word.SignedWord; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.code.FrameInfoQueryResult; +import com.oracle.svm.core.code.FrameSourceInfo; +import com.oracle.svm.core.heap.ReferenceAccess; +import com.oracle.svm.core.interpreter.InterpreterSupport; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; + +import jdk.graal.compiler.word.Word; +import jdk.vm.ci.meta.LineNumberTable; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +public final class InterpreterSupportImpl extends InterpreterSupport { + private final int bciSlot; + private final int interpretedMethodSlot; + private final int interpretedFrameSlot; + + InterpreterSupportImpl(int bciSlot, int interpretedMethodSlot, int interpretedFrameSlot) { + this.bciSlot = bciSlot; + this.interpretedMethodSlot = interpretedMethodSlot; + this.interpretedFrameSlot = interpretedFrameSlot; + } + + @Override + public boolean isInterpreterRoot(Class clazz) { + return Interpreter.Root.class.equals(clazz); + } + + private static int readInt(Pointer addr, SignedWord offset) { + return addr.readInt(offset); + } + + @SuppressWarnings("unchecked") + private static T readObject(Pointer addr, SignedWord offset, boolean compressed) { + Word p = ((Word) addr).add(offset); + Object obj = ReferenceAccess.singleton().readObjectAt(p, compressed); + return (T) obj; + } + + private InterpreterResolvedJavaMethod readInterpretedMethod(FrameInfoQueryResult frameInfo, Pointer sp) { + FrameInfoQueryResult.ValueInfo valueInfo = frameInfo.getValueInfos()[interpretedMethodSlot]; + return readObject(sp, WordFactory.signed(valueInfo.getData()), valueInfo.isCompressedReference()); + } + + private int readBCI(FrameInfoQueryResult frameInfo, Pointer sp) { + FrameInfoQueryResult.ValueInfo valueInfo = frameInfo.getValueInfos()[bciSlot]; + return readInt(sp, WordFactory.signed(valueInfo.getData())); + } + + private InterpreterFrame readInterpreterFrame(FrameInfoQueryResult frameInfo, Pointer sp) { + FrameInfoQueryResult.ValueInfo valueInfo = frameInfo.getValueInfos()[interpretedFrameSlot]; + return readObject(sp, WordFactory.signed(valueInfo.getData()), valueInfo.isCompressedReference()); + } + + @Override + public FrameSourceInfo getInterpretedMethodFrameInfo(FrameInfoQueryResult frameInfo, Pointer sp) { + if (!isInterpreterRoot(frameInfo.getSourceClass())) { + throw VMError.shouldNotReachHereAtRuntime(); + } + InterpreterResolvedJavaMethod interpretedMethod = readInterpretedMethod(frameInfo, sp); + int bci = readBCI(frameInfo, sp); + InterpreterFrame interpreterFrame = readInterpreterFrame(frameInfo, sp); + Class interpretedClass = ((InterpreterResolvedJavaType) interpretedMethod.getDeclaringClass()).getJavaClass(); + String sourceMethodName = interpretedMethod.getName(); + LineNumberTable lineNumberTable = interpretedMethod.getLineNumberTable(); + + int sourceLineNumber = -1; // unknown + if (lineNumberTable != null) { + sourceLineNumber = lineNumberTable.getLineNumber(bci); + } + return new InterpreterFrameSourceInfo(interpretedClass, sourceMethodName, sourceLineNumber, bci, interpretedMethod, interpreterFrame); + } + + @Platforms(Platform.HOSTED_ONLY.class) + @Override + public void buildMethodIdMapping(ResolvedJavaMethod[] encodedMethods) { + if (InterpreterOptions.DebuggerWithInterpreter.getValue()) { + assert ImageSingletons.contains(DebuggerSupport.class); + ImageSingletons.lookup(DebuggerSupport.class).buildMethodIdMapping(encodedMethods); + } + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterToVM.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterToVM.java new file mode 100644 index 000000000000..65ae4e36c7d7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterToVM.java @@ -0,0 +1,906 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter; + +import static com.oracle.svm.interpreter.InterpreterOptions.InterpreterTraceSupport; +import static com.oracle.svm.interpreter.InterpreterOptions.DebuggerWithInterpreter; +import static com.oracle.svm.interpreter.InterpreterUtil.traceInterpreter; +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.VTBL_NO_ENTRY; +import static com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod.VTBL_ONE_IMPL; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; + +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.graal.snippets.OpenTypeWorldDispatchTableSnippets; +import com.oracle.svm.core.hub.RuntimeClassLoading; +import com.oracle.svm.interpreter.metadata.ReferenceConstant; +import jdk.vm.ci.meta.PrimitiveConstant; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.MissingReflectionRegistrationError; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.word.WordBase; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.classinitialization.EnsureClassInitializedNode; +import com.oracle.svm.core.config.ConfigurationValues; +import com.oracle.svm.core.graal.meta.KnownOffsets; +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.jdk.InternalVMMethod; +import com.oracle.svm.core.monitor.MonitorInflationCause; +import com.oracle.svm.core.monitor.MonitorSupport; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaField; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedObjectType; + +import jdk.graal.compiler.api.directives.GraalDirectives; +import jdk.graal.compiler.core.common.SuppressFBWarnings; +import jdk.graal.compiler.nodes.java.ArrayLengthNode; +import jdk.graal.compiler.word.Word; +import jdk.internal.misc.Unsafe; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaType; + +@InternalVMMethod +public final class InterpreterToVM { + + private static final JavaKind WORD_KIND = ConfigurationValues.getTarget().wordJavaKind; + + static { + VMError.guarantee(WORD_KIND == JavaKind.Int || WORD_KIND == JavaKind.Long); + } + + public static JavaKind wordJavaKind() { + return WORD_KIND; + } + + private InterpreterToVM() { + throw VMError.shouldNotReachHereAtRuntime(); + } + + // region Get (array) operations + + public static int getArrayInt(int index, int[] array) throws SemanticJavaException { + assert array != null; + try { + return array[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static Object getArrayObject(int index, Object[] array) throws SemanticJavaException { + assert array != null; + try { + return array[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static long getArrayLong(int index, long[] array) throws SemanticJavaException { + assert array != null; + try { + return array[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static WordBase getArrayWord(int index, WordBase[] wordArray) throws SemanticJavaException { + assert wordArray != null; + try { + return wordArray[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static float getArrayFloat(int index, float[] array) throws SemanticJavaException { + assert array != null; + try { + return array[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static double getArrayDouble(int index, double[] array) throws SemanticJavaException { + assert array != null; + try { + return array[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static byte getArrayByte(int index, Object array) throws SemanticJavaException { + assert array != null; + try { + if (array instanceof byte[]) { + return ((byte[]) array)[index]; + } else { + return ((boolean[]) array)[index] ? (byte) 1 : 0; + } + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static char getArrayChar(int index, char[] array) throws SemanticJavaException { + assert array != null; + try { + return array[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static short getArrayShort(int index, short[] array) throws SemanticJavaException { + assert array != null; + try { + return array[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + // endregion + + // region Set (array) operations + + public static void setArrayInt(int value, int index, int[] array) throws SemanticJavaException { + assert array != null; + try { + array[index] = value; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static void setArrayLong(long value, int index, long[] array) throws SemanticJavaException { + assert array != null; + try { + array[index] = value; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static void setArrayWord(WordBase value, int index, WordBase[] array) throws SemanticJavaException { + assert array != null; + try { + array[index] = value; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static void setArrayFloat(float value, int index, float[] array) throws SemanticJavaException { + assert array != null; + try { + array[index] = value; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static void setArrayDouble(double value, int index, double[] array) throws SemanticJavaException { + assert array != null; + try { + array[index] = value; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static void setArrayByte(byte value, int index, /* byte[].class or boolean[].class */ Object array) throws SemanticJavaException { + assert array != null; + try { + if (array instanceof byte[]) { + ((byte[]) array)[index] = value; + } else { + ((boolean[]) array)[index] = (value & 1) != 0; // masked from Java 9+. + } + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static void setArrayChar(char value, int index, char[] array) throws SemanticJavaException { + assert array != null; + try { + array[index] = value; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static void setArrayShort(short value, int index, short[] array) throws SemanticJavaException { + assert array != null; + try { + array[index] = value; + } catch (ArrayIndexOutOfBoundsException e) { + throw SemanticJavaException.raise(e); + } + } + + public static void setArrayObject(Object value, int index, Object[] array) throws SemanticJavaException { + assert array != null; + try { + array[index] = value; + } catch (ArrayIndexOutOfBoundsException | ArrayStoreException e) { + throw SemanticJavaException.raise(e); + } + } + + // endregion Set (array) operations + + // region Monitor enter/exit + + public static void monitorEnter(InterpreterFrame frame, Object obj) throws SemanticJavaException { + assert obj != null; + MonitorSupport.singleton().monitorEnter(obj, MonitorInflationCause.MONITOR_ENTER); + frame.addLock(obj); + } + + @SuppressFBWarnings(value = "IMSE_DONT_CATCH_IMSE", justification = "Intentional.") + public static void monitorExit(InterpreterFrame frame, Object obj) throws SemanticJavaException { + assert obj != null; + try { + MonitorSupport.singleton().monitorExit(obj, MonitorInflationCause.VM_INTERNAL); + // GR-55049: Ensure that SVM doesn't allow non-structured locking. + frame.removeLock(obj); + } catch (IllegalMonitorStateException e) { + // GR-55050: Hide intermediate frames on exception. + throw SemanticJavaException.raise(e); + } + } + + @SuppressFBWarnings(value = "IMSE_DONT_CATCH_IMSE", justification = "Intentional.") + public static void releaseInterpreterFrameLocks(@SuppressWarnings("unused") InterpreterFrame frame) throws SemanticJavaException { + Object[] locks = frame.getLocks(); + for (int i = 0; i < locks.length; ++i) { + Object ref = locks[i]; + if (ref != null) { + try { + MonitorSupport.singleton().monitorExit(ref, MonitorInflationCause.VM_INTERNAL); + // GR-55049: Ensure that SVM doesn't allow non-structured locking. + locks[i] = null; + } catch (IllegalMonitorStateException e) { + // GR-55050: Hide intermediate frames on exception. + throw SemanticJavaException.raise(e); + } + } + } + } + + // endregion + + private static final Unsafe U = initUnsafe(); + + private static Unsafe initUnsafe() { + try { + return Unsafe.getUnsafe(); + } catch (SecurityException se) { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + return (Unsafe) theUnsafe.get(Unsafe.class); + } catch (Exception e) { + throw new RuntimeException("Exception while trying to get Unsafe", e); + } + } + } + + public static WordBase getFieldWord(Object obj, InterpreterResolvedJavaField wordField) throws SemanticJavaException { + assert obj != null; + assert wordField.getType().isWordType(); + return switch (wordJavaKind()) { + case Long -> WordFactory.signed(getFieldLong(obj, wordField)); + case Int -> WordFactory.signed(getFieldInt(obj, wordField)); + default -> throw VMError.shouldNotReachHere("Unexpected word kind " + wordJavaKind()); + }; + } + + public static boolean getFieldBoolean(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + return field.getUnmaterializedConstant().asBoolean(); + } + if (field.isVolatile()) { + return U.getBooleanVolatile(obj, field.getOffset()); + } else { + return U.getBoolean(obj, field.getOffset()); + } + } + + public static int getFieldInt(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + return field.getUnmaterializedConstant().asInt(); + } + if (field.isVolatile()) { + return U.getIntVolatile(obj, field.getOffset()); + } else { + return U.getInt(obj, field.getOffset()); + } + } + + public static long getFieldLong(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + return field.getUnmaterializedConstant().asLong(); + } + if (field.isVolatile()) { + return U.getLongVolatile(obj, field.getOffset()); + } else { + return U.getLong(obj, field.getOffset()); + } + } + + public static byte getFieldByte(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + return (byte) field.getUnmaterializedConstant().asInt(); + } + if (field.isVolatile()) { + return U.getByteVolatile(obj, field.getOffset()); + } else { + return U.getByte(obj, field.getOffset()); + } + } + + public static short getFieldShort(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + return (short) field.getUnmaterializedConstant().asInt(); + } + if (field.isVolatile()) { + return U.getShortVolatile(obj, field.getOffset()); + } else { + return U.getShort(obj, field.getOffset()); + } + } + + public static float getFieldFloat(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + return field.getUnmaterializedConstant().asFloat(); + } + if (field.isVolatile()) { + return U.getFloatVolatile(obj, field.getOffset()); + } else { + return U.getFloat(obj, field.getOffset()); + } + } + + public static double getFieldDouble(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + return field.getUnmaterializedConstant().asDouble(); + } + if (field.isVolatile()) { + return U.getDoubleVolatile(obj, field.getOffset()); + } else { + return U.getDouble(obj, field.getOffset()); + } + } + + public static Object getFieldObject(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + JavaConstant constant = field.getUnmaterializedConstant(); + if (JavaConstant.NULL_POINTER.equals(constant)) { + return null; + } + VMError.guarantee(!constant.equals(PrimitiveConstant.ILLEGAL), Interpreter.FAILURE_CONSTANT_NOT_PART_OF_IMAGE_HEAP); + VMError.guarantee(constant.isNonNull(), Interpreter.FAILURE_CONSTANT_NOT_PART_OF_IMAGE_HEAP); + return ((ReferenceConstant) constant).getReferent(); + } + if (field.isVolatile()) { + return U.getReferenceVolatile(obj, field.getOffset()); + } else { + return U.getReference(obj, field.getOffset()); + } + } + + public static char getFieldChar(Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isUnmaterializedConstant()) { + return (char) field.getUnmaterializedConstant().asInt(); + } + if (field.isVolatile()) { + return U.getCharVolatile(obj, field.getOffset()); + } else { + return U.getChar(obj, field.getOffset()); + } + } + + public static void setFieldBoolean(boolean value, Object obj, InterpreterResolvedJavaField field) { + if (field.isVolatile()) { + U.putBooleanVolatile(obj, field.getOffset(), value); + } else { + U.putBoolean(obj, field.getOffset(), value); + } + } + + public static void setFieldByte(byte value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isVolatile()) { + U.putByteVolatile(obj, field.getOffset(), value); + } else { + U.putByte(obj, field.getOffset(), value); + } + } + + public static void setFieldChar(char value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isVolatile()) { + U.putCharVolatile(obj, field.getOffset(), value); + } else { + U.putChar(obj, field.getOffset(), value); + } + } + + public static void setFieldShort(short value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isVolatile()) { + U.putShortVolatile(obj, field.getOffset(), value); + } else { + U.putShort(obj, field.getOffset(), value); + } + } + + public static void setFieldInt(int value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + assert field.getJavaKind() == JavaKind.Int || field.getType().isWordType(); + if (field.isVolatile()) { + U.putIntVolatile(obj, field.getOffset(), value); + } else { + U.putInt(obj, field.getOffset(), value); + } + } + + public static void setFieldLong(long value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + assert field.getJavaKind() == JavaKind.Long || field.getType().isWordType(); + if (field.isVolatile()) { + U.putLongVolatile(obj, field.getOffset(), value); + } else { + U.putLong(obj, field.getOffset(), value); + } + } + + public static void setFieldWord(WordBase value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + switch (wordJavaKind()) { + case Int -> setFieldInt((int) value.rawValue(), obj, field); + case Long -> setFieldLong(value.rawValue(), obj, field); + default -> throw VMError.shouldNotReachHere("Unexpected word kind " + wordJavaKind()); + } + } + + public static void setFieldFloat(float value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isVolatile()) { + U.putFloatVolatile(obj, field.getOffset(), value); + } else { + U.putFloat(obj, field.getOffset(), value); + } + } + + public static void setFieldDouble(double value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isVolatile()) { + U.putDoubleVolatile(obj, field.getOffset(), value); + } else { + U.putDouble(obj, field.getOffset(), value); + } + } + + public static void setFieldObject(Object value, Object obj, InterpreterResolvedJavaField field) { + assert obj != null; + if (field.isVolatile()) { + U.putReferenceVolatile(obj, field.getOffset(), value); + } else { + U.putReference(obj, field.getOffset(), value); + } + } + + /** + * Subtyping among Array Types The following rules define the direct supertype relation among + * array types: + * + *
    + *
  • If S and T are both reference types, then S[] >1 T[] iff S >1 T. + *
  • Object >1 Object[] + *
  • Cloneable >1 Object[] + *
  • java.io.Serializable >1 Object[] + *
  • If P is a primitive type, then: Object >1 P[] Cloneable >1 P[] java.io.Serializable >1 + * P[] + *
+ */ + public static boolean instanceOf(Object instance, InterpreterResolvedJavaType typeToCheck) { + return instanceOf(instance, typeToCheck.getJavaClass()); + } + + public static boolean instanceOf(Object instance, Class classToCheck) { + if (instance == null) { + return false; + } + return classToCheck.isAssignableFrom(instance.getClass()); + } + + private static String cannotCastMsg(Object instance, Class clazz) { + return "Cannot cast " + instance.getClass().getName() + " to " + clazz.getName(); + } + + public static Object checkCast(Object instance, Class classToCheck) throws SemanticJavaException { + assert classToCheck != null; + // Avoid Class#cast since it pollutes stack traces. + if (GraalDirectives.injectBranchProbability(GraalDirectives.SLOWPATH_PROBABILITY, instance != null && !instanceOf(instance, classToCheck))) { + throw SemanticJavaException.raise(new ClassCastException(cannotCastMsg(instance, classToCheck))); + } + return instance; + } + + public static Object checkCast(Object instance, InterpreterResolvedJavaType typeToCheck) throws SemanticJavaException { + return checkCast(instance, typeToCheck.getJavaClass()); + } + + public static int arrayLength(Object array) { + assert array != null && array.getClass().isArray(); + return ArrayLengthNode.arrayLength(array); + } + + public static Object createNewReference(InterpreterResolvedJavaType klass) throws SemanticJavaException { + assert !klass.isPrimitive(); + Class clazz = klass.getJavaClass(); + ensureClassInitialized(clazz); + try { + // GR-55050: Ensure that the type can be allocated on SVM. + // At this point failing allocation should only imply OutOfMemoryError or + // StackOverflowError which are handled specially by the interpreter. + // GR-55050: Hide/remove the Unsafe#allocateInstance frame e.g. use a + // DynamicNewInstanceNode intrinsic. + return U.allocateInstance(clazz); + } catch (InstantiationException | IllegalArgumentException | MissingReflectionRegistrationError e) { + throw SemanticJavaException.raise(e); + } + } + + public static Object createNewPrimitiveArray(byte jvmPrimitiveType, int length) throws SemanticJavaException { + try { + return switch (jvmPrimitiveType) { + case 4 -> new boolean[length]; + case 5 -> new char[length]; + case 6 -> new float[length]; + case 7 -> new double[length]; + case 8 -> new byte[length]; + case 9 -> new short[length]; + case 10 -> new int[length]; + case 11 -> new long[length]; + default -> throw VMError.shouldNotReachHereAtRuntime(); + }; + } catch (NegativeArraySizeException e) { + throw SemanticJavaException.raise(e); + } + } + + public static Object createNewReferenceArray(InterpreterResolvedJavaType componentType, int length) throws SemanticJavaException { + assert componentType.getJavaKind() != JavaKind.Void; + assert !componentType.getJavaKind().isPrimitive(); + assert getDimensions(componentType) + 1 <= 255; + if (length < 0) { + throw SemanticJavaException.raise(new NegativeArraySizeException(String.valueOf(length))); + } + // GR-55050: Ensure that the array type can be allocated on SVM. + // At this point failing allocation should only imply OutOfMemoryError or + // StackOverflowError which are handled specially by the interpreter. + // GR-55050: Hide/remove the Array.newInstance (and other intermediate) frames + // e.g. use a DynamicNewArrayInstanceNode intrinsic. + return Array.newInstance(componentType.getJavaClass(), length); + } + + private static int getDimensions(ResolvedJavaType object) { + int dimensions = 0; + for (ResolvedJavaType elem = object; elem.isArray(); elem = elem.getComponentType()) { + dimensions++; + } + return dimensions; + } + + public static Object createMultiArray(InterpreterResolvedJavaType multiArrayType, int[] dimensions) throws SemanticJavaException { + assert dimensions.length > 0; + assert getDimensions(multiArrayType) >= dimensions.length; + assert getDimensions(multiArrayType) <= 255; + // GR-55050: Ensure that the array type can be allocated on SVM. + InterpreterResolvedJavaType component = multiArrayType; + for (int d : dimensions) { + if (d < 0) { + throw SemanticJavaException.raise(new NegativeArraySizeException(String.valueOf(d))); + } + component = (InterpreterResolvedJavaType) component.getComponentType(); + } + // At this point failing allocation should only imply OutOfMemoryError or + // StackOverflowError which are handled specially by the interpreter. + // GR-55050: Hide/remove the Array.newInstance (and other intermediate) frames + // e.g. use a DynamicNewArrayInstanceNode intrinsic. + return Array.newInstance(component.getJavaClass(), dimensions); + } + + public static void ensureClassInitialized(InterpreterResolvedObjectType type) { + ensureClassInitialized(type.getJavaClass()); + } + + /** + * Ensures that the given class is initialized, which may execute the static initializer of the + * given class it's superclasses/superinterfaces. Exceptions thrown by class initialization are + * propagated as a semantic/valid Java exception to the interpreter. + */ + public static void ensureClassInitialized(Class clazz) throws SemanticJavaException { + assert clazz != null; + try { + EnsureClassInitializedNode.ensureClassInitialized(clazz); + } catch (Error e) { + // Only Error is expected here. + throw SemanticJavaException.raise(e); + } + } + + static CFunctionPointer peekAtSVMVTable(Class seedClass, Class thisClass, int vTableIndex, boolean isInvokeInterface) { + DynamicHub seedHub = DynamicHub.fromClass(seedClass); + DynamicHub thisHub = DynamicHub.fromClass(thisClass); + + int vtableOffset = KnownOffsets.singleton().getVTableOffset(vTableIndex, false); + + if (SubstrateOptions.useClosedTypeWorldHubLayout()) { + vtableOffset += KnownOffsets.singleton().getVTableBaseOffset(); + } else { + VMError.guarantee(seedHub.isInterface() == isInvokeInterface); + + if (!seedHub.isInterface()) { + vtableOffset += KnownOffsets.singleton().getVTableBaseOffset(); + } else { + vtableOffset += (int) OpenTypeWorldDispatchTableSnippets.determineITableStartingOffset(thisHub, seedHub.getTypeID()); + } + } + return Word.objectToTrackedPointer(thisHub).readWord(vtableOffset); + } + + private static InterpreterResolvedJavaMethod peekAtInterpreterVTable(Class seedClass, Class thisClass, int vTableIndex, boolean isInvokeInterface) { + ResolvedJavaType thisType; + if (DebuggerWithInterpreter.getValue()) { + DebuggerSupport interpreterSupport = ImageSingletons.lookup(DebuggerSupport.class); + thisType = interpreterSupport.getUniverse().lookupType(thisClass); + } else { + assert RuntimeClassLoading.isSupported(); + throw VMError.unimplemented("obtain java type with vtable mirror"); + } + VMError.guarantee(thisType != null); + VMError.guarantee(thisType instanceof InterpreterResolvedObjectType); + + InterpreterResolvedJavaMethod[] vTable = ((InterpreterResolvedObjectType) thisType).getVtable(); + VMError.guarantee(vTable != null); + + DynamicHub seedHub = DynamicHub.fromClass(seedClass); + + if (SubstrateOptions.useClosedTypeWorldHubLayout()) { + VMError.guarantee(vTableIndex > 0 && vTableIndex < vTable.length); + return vTable[vTableIndex]; + } else { + VMError.guarantee(seedHub.isInterface() == isInvokeInterface); + + if (!seedHub.isInterface()) { + return vTable[vTableIndex]; + } else { + int iTableStartingIndex = determineITableStartingIndex(DynamicHub.fromClass(thisClass), seedHub.getTypeID()); + return vTable[iTableStartingIndex + vTableIndex]; + } + } + } + + private static int determineITableStartingIndex(DynamicHub thisHub, int interfaceID) { + /* + * iTableStartingOffset includes the initial offset to the vtable array and describes an + * offset (not index) + */ + long iTableStartingOffset = OpenTypeWorldDispatchTableSnippets.determineITableStartingOffset(thisHub, interfaceID); + + int vtableBaseOffset = KnownOffsets.singleton().getVTableBaseOffset(); + int vtableEntrySize = KnownOffsets.singleton().getVTableEntrySize(); + + return (int) (iTableStartingOffset - vtableBaseOffset) / vtableEntrySize; + } + + public static Object dispatchInvocation(InterpreterResolvedJavaMethod seedMethod, Object[] calleeArgs, boolean isVirtual0, boolean forceStayInInterpreter, boolean preferStayInInterpreter, + boolean isInvokeInterface) + throws SemanticJavaException { + boolean goThroughPLT; + boolean isVirtual = isVirtual0; + + if (forceStayInInterpreter) { + // Force execution in the interpreter, transitively, for all callees in the call + // subtree. + goThroughPLT = false; + } else { + // Not forced to transitively "stay in interpreter"; but still; it may be "preferred" to + // execute this callee (and only this one) in the interpreter, if possible e.g. Step + // Into. + goThroughPLT = !preferStayInInterpreter; + } + + InterpreterResolvedObjectType seedDeclaringClass = seedMethod.getDeclaringClass(); + if (seedMethod.isStatic()) { + InterpreterUtil.guarantee(!isVirtual, "no virtual calls for static method %s", seedMethod); + ensureClassInitialized(seedDeclaringClass); + } + + CFunctionPointer calleeFtnPtr = WordFactory.nullPointer(); + + if (goThroughPLT) { + if (seedMethod.hasNativeEntryPoint()) { + calleeFtnPtr = seedMethod.getNativeEntryPoint(); + traceInterpreter("got native entry point: ").hex(calleeFtnPtr).newline(); + } else if (seedMethod.getVTableIndex() == VTBL_NO_ENTRY) { + /* + * does not always hold. Counter example: j.io.BufferedWriter::min, because it gets + * inlined + */ + // InterpreterUtil.guarantee(!isVirtual, "leaveInterpreter is virtual %s", + // seedMethod); + goThroughPLT = false; + + /* arguments to Log methods might have side-effects */ + if (InterpreterTraceSupport.getValue()) { + traceInterpreter("fall back to interp for compile entry ").string(seedMethod.toString()).string(" because it has not been compiled.").newline(); + } + } else if (seedMethod.getVTableIndex() == VTBL_ONE_IMPL) { + goThroughPLT = seedMethod.getOneImplementation().hasNativeEntryPoint(); + } else if (!isVirtual && seedMethod.hasVTableIndex()) { + goThroughPLT = false; + /* arguments to Log methods might have side-effects */ + if (InterpreterTraceSupport.getValue()) { + traceInterpreter("invokespecial: ").string(seedMethod.toString()).newline(); + } + } else if (isVirtual && !seedMethod.hasVTableIndex()) { + VMError.shouldNotReachHere("cannot do virtual dispatch without vtable index"); + } + } + + // Arrays have no vtable. + if (isVirtual && (seedMethod.isFinalFlagSet() || calleeArgs[0].getClass().isArray() || seedDeclaringClass.isLeaf() || seedMethod.isPrivate())) { + isVirtual = false; + /* arguments to Log methods might have side-effects */ + if (InterpreterTraceSupport.getValue()) { + traceInterpreter("reverting virtual call to invokespecial: ").string(seedMethod.toString()).newline(); + } + } + + InterpreterResolvedJavaMethod targetMethod = seedMethod; + if (isVirtual && seedMethod.hasVTableIndex()) { + /* vtable dispatch */ + + VMError.guarantee(seedMethod.hasReceiver()); + + Class thisClazz = calleeArgs[0].getClass(); + Class seedClazz = seedDeclaringClass.getJavaClass(); + int vtableIndex = seedMethod.getVTableIndex(); + + if (goThroughPLT) { + // determine virtual call target via SVM vtable dispatch + calleeFtnPtr = peekAtSVMVTable(seedClazz, thisClazz, vtableIndex, isInvokeInterface); + + if (calleeFtnPtr.equal(InterpreterMethodPointerHolder.getMethodNotCompiledHandler())) { + // can happen e.g. due to devirtualization, need to stay in interpreter in + // this scenario + goThroughPLT = false; + + /* arguments to Log methods might have side-effects */ + if (InterpreterTraceSupport.getValue()) { + traceInterpreter("fall back to interp (vtable entry) for compile entry ").string(seedMethod.toString()).string(" because it has not been compiled.").newline(); + } + } + } + + /* always resolve the right target method in the interpreter universe */ + targetMethod = peekAtInterpreterVTable(seedClazz, thisClazz, vtableIndex, isInvokeInterface); + } else if (seedMethod.getVTableIndex() == VTBL_ONE_IMPL) { + targetMethod = seedMethod.getOneImplementation(); + /* arguments to Log methods might have side-effects */ + if (InterpreterTraceSupport.getValue()) { + traceInterpreter("found oneImpl: ").string(targetMethod.toString()); + if (goThroughPLT) { + calleeFtnPtr = targetMethod.getNativeEntryPoint(); + traceInterpreter(" ... with compiled entry=").hex(calleeFtnPtr); + } + traceInterpreter("").newline(); + } + VMError.guarantee(targetMethod != null, "VTBL_ONE_IMPL implies that oneImplementation is available in seedMethod"); + } + + if (!targetMethod.hasBytecodes() && !goThroughPLT && calleeFtnPtr.isNonNull()) { + goThroughPLT = true; + /* arguments to Log methods might have side-effects */ + if (InterpreterTraceSupport.getValue()) { + traceInterpreter("cannot interpret ").string(targetMethod.toString()).string(" falling back to compiled version ").hex(calleeFtnPtr).newline(); + } + } + + if (!goThroughPLT && targetMethod.isNative()) { + /* no way to execute target in interpreter, fall back to compiled code */ + /* example: MethodHandle.invokeBasic */ + VMError.guarantee(targetMethod.hasNativeEntryPoint()); + calleeFtnPtr = targetMethod.getNativeEntryPoint(); + VMError.guarantee(calleeFtnPtr.isNonNull()); + goThroughPLT = true; + } + + /* arguments to Log methods might have side-effects */ + if (InterpreterOptions.InterpreterTraceSupport.getValue()) { + traceInterpreter(" ".repeat(Interpreter.logIndent.get())) + .string(" -> calling (") + .string(goThroughPLT ? "plt" : "interp").string(") ") + .string(targetMethod.hasNativeEntryPoint() ? "(compiled entry available) " : ""); + if (targetMethod.hasNativeEntryPoint()) { + traceInterpreter("(addr: ").hex(calleeFtnPtr).string(" ) "); + } + traceInterpreter(targetMethod.getDeclaringClass().getName()) + .string("::").string(targetMethod.getName()) + .string(targetMethod.getSignature().toMethodDescriptor()) + .newline(); + } + + Object retObj = null; + if (goThroughPLT) { + VMError.guarantee(!forceStayInInterpreter); + VMError.guarantee(calleeFtnPtr.isNonNull()); + + // wrapping of exceptions is done in leaveInterpreter + retObj = InterpreterStubSection.leaveInterpreter(calleeFtnPtr, targetMethod, targetMethod.getDeclaringClass(), calleeArgs); + } else { + try { + retObj = Interpreter.execute(targetMethod, calleeArgs, forceStayInInterpreter); + } catch (Throwable e) { + // Exceptions coming from calls are valid, semantic Java exceptions. + throw SemanticJavaException.raise(e); + } + } + + return retObj; + } + + public static Object nullCheck(Object value) throws SemanticJavaException { + if (GraalDirectives.injectBranchProbability(GraalDirectives.FASTPATH_PROBABILITY, value != null)) { + return value; + } + throw SemanticJavaException.raise(new NullPointerException()); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterUtil.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterUtil.java new file mode 100644 index 000000000000..fe4d15927426 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/InterpreterUtil.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter; + +import com.oracle.svm.core.log.Log; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.MetadataUtil; + +public class InterpreterUtil { + + /** + * Alternative to {@link VMError#guarantee(boolean, String, Object)} that avoids + * {@link String#format(String, Object...)} . + */ + public static void guarantee(boolean condition, String simpleFormat, Object arg1) { + if (!condition) { + VMError.guarantee(condition, MetadataUtil.fmt(simpleFormat, arg1)); + } + } + + /** + * Build time logging. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public static void log(String msg) { + if (InterpreterOptions.InterpreterBuildTimeLogging.getValue()) { + System.out.println(msg); + } + } + + /** + * Build time logging. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public static void log(String simpleFormat, Object arg1) { + if (InterpreterOptions.InterpreterBuildTimeLogging.getValue()) { + System.out.println(MetadataUtil.fmt(simpleFormat, arg1)); + } + } + + /** + * Build time logging. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public static void log(String simpleFormat, Object arg1, Object arg2) { + if (InterpreterOptions.InterpreterBuildTimeLogging.getValue()) { + System.out.println(MetadataUtil.fmt(simpleFormat, arg1, arg2)); + } + } + + /** + * Build time logging. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public static void log(String simpleFormat, Object arg1, Object arg2, Object arg3) { + if (InterpreterOptions.InterpreterBuildTimeLogging.getValue()) { + System.out.println(MetadataUtil.fmt(simpleFormat, arg1, arg2, arg3)); + } + } + + /** + * Build time logging. + */ + @Platforms(Platform.HOSTED_ONLY.class) + public static void log(Throwable t) { + if (InterpreterOptions.InterpreterBuildTimeLogging.getValue()) { + t.printStackTrace(System.out); + } + } + + public static Log traceInterpreter(String msg) { + if (InterpreterOptions.InterpreterTraceSupport.getValue()) { + if (InterpreterOptions.InterpreterTrace.getValue()) { + return Log.log().string(msg); + } + } + return Log.noopLog(); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/ReturnAddress.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/ReturnAddress.java new file mode 100644 index 000000000000..d38b0eb57e86 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/ReturnAddress.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter; + +/** + * Represents the "returnAddress" type, used for JSR/RET bytecodes. + * + * @see The + * returnAddress Type and Values + */ +record ReturnAddress(int bci) { + static ReturnAddress create(int bci) { + return new ReturnAddress(bci); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/SemanticJavaException.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/SemanticJavaException.java new file mode 100644 index 000000000000..f3b749670982 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/SemanticJavaException.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter; + +import com.oracle.svm.core.jdk.InternalVMMethod; + +/** + * Wraps exceptions thrown by the interpreted code or by compiled code. This is a way to + * differentiate between exceptions caused by interpreter itself vs. the code it executes. + */ +@InternalVMMethod +public final class SemanticJavaException extends RuntimeException { + @java.io.Serial static final long serialVersionUID = 8271499373291031203L; + + private SemanticJavaException(Throwable cause) { + super(cause); + } + + @Override + @SuppressWarnings("sync-override") + public Throwable fillInStackTrace() { + return this; + } + + public static RuntimeException raise(Throwable cause) { + assert cause != null && !(cause instanceof SemanticJavaException); + throw new SemanticJavaException(cause); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/classfile/ClassFile.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/classfile/ClassFile.java new file mode 100644 index 000000000000..cde5a5534f84 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/classfile/ClassFile.java @@ -0,0 +1,1116 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter.classfile; + +import static com.oracle.svm.interpreter.metadata.Bytecodes.ANEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.CHECKCAST; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GETFIELD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.GETSTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INSTANCEOF; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEDYNAMIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEINTERFACE; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESPECIAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKESTATIC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.INVOKEVIRTUAL; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC2_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.LDC_W; +import static com.oracle.svm.interpreter.metadata.Bytecodes.MULTIANEWARRAY; +import static com.oracle.svm.interpreter.metadata.Bytecodes.NEW; +import static com.oracle.svm.interpreter.metadata.Bytecodes.PUTFIELD; +import static com.oracle.svm.interpreter.metadata.Bytecodes.PUTSTATIC; + +import java.lang.invoke.MethodType; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import com.oracle.svm.interpreter.metadata.ReferenceConstant; +import org.graalvm.collections.Pair; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.BytecodeStream; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedObjectType; +import com.oracle.svm.interpreter.metadata.MetadataUtil; + +import jdk.vm.ci.meta.ConstantPool; +import jdk.vm.ci.meta.ExceptionHandler; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaField; +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.LineNumberTable; +import jdk.vm.ci.meta.Local; +import jdk.vm.ci.meta.LocalVariableTable; +import jdk.vm.ci.meta.ModifiersProvider; +import jdk.vm.ci.meta.PrimitiveConstant; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; +import jdk.vm.ci.meta.Signature; + +/** + * Utility to re-create a .class file representation from a {@link ResolvedJavaType}. + * + *

+ * .class files and JVMCI data structures are not equivalent, generated .class files may not be + * loadable on a JVM. + * + *

+ * Known issues: + *

    + *
  • Incorrect/hardcoded .class file version
  • + *
  • Missing attributes
  • + *
  • INVOKEDYNAMIC (missing BootstrapMethods) is not supported
  • + *
+ */ +@Platforms(Platform.HOSTED_ONLY.class) +public final class ClassFile { + + public static final int MAGIC = 0xCAFEBABE; + + // GR-55048: Find .class file version from ResolvedJavaType. + public static final int MAJOR_VERSION = 55; + public static final int MINOR_VERSION = 0; + + public static final byte CONSTANT_Utf8 = 1; + public static final byte CONSTANT_Integer = 3; + public static final byte CONSTANT_Float = 4; + public static final byte CONSTANT_Long = 5; + public static final byte CONSTANT_Double = 6; + public static final byte CONSTANT_Class = 7; + public static final byte CONSTANT_String = 8; + public static final byte CONSTANT_Fieldref = 9; + public static final byte CONSTANT_Methodref = 10; + public static final byte CONSTANT_InterfaceMethodref = 11; + public static final byte CONSTANT_NameAndType = 12; + public static final byte CONSTANT_MethodHandle = 15; + public static final byte CONSTANT_MethodType = 16; + @SuppressWarnings("unused") public static final byte CONSTANT_Dynamic = 17; + @SuppressWarnings("unused") public static final byte CONSTANT_InvokeDynamic = 18; + public static final byte CONSTANT_Module = 19; + public static final byte CONSTANT_Package = 20; + + private final OutStream classFile = new OutStream(); + private final OutStream constantPool = new OutStream(); + + private int constantPoolEntryCount; + + private final Map fieldRefCache = new HashMap<>(); + private final Map, Integer> nameAndTypeCache = new HashMap<>(); + private final Map utf8Cache = new HashMap<>(); + + private final Map intCache = new HashMap<>(); + private final Map longCache = new HashMap<>(); + + // Map on the float bit pattern to account for NaNs. + private final Map floatCache = new HashMap<>(); + + // Map on the double bit pattern to account for NaNs. + private final Map doubleCache = new HashMap<>(); + + private final Map stringCache = new HashMap<>(); + private final Map classCache = new HashMap<>(); + private final Map methodRefCache = new HashMap<>(); + private final Map interfaceMethodRefCache = new HashMap<>(); + + private final Map methodTypeCache = new HashMap<>(); + private final Map, Integer> methodHandleCache = new HashMap<>(); + + private final Map moduleCache = new HashMap<>(); + private final Map packageCache = new HashMap<>(); + + public ClassFile(InterpreterUniverse universe, Function extractConstantValue) { + this.universe = universe; + this.extractConstantValue = extractConstantValue; + } + + private int utf8(String str) { + return utf8Cache.computeIfAbsent(str, + key -> { + constantPool.writeU1(CONSTANT_Utf8); + constantPool.writeUTF(str); // writeUTF prepends the length + return ++constantPoolEntryCount; + }); + } + + private int longConstant(long value) { + return longCache.computeIfAbsent(value, + key -> { + constantPool.writeU1(CONSTANT_Long); + constantPool.writeLong(value); + // All 8-byte constants take up two entries in the constant_pool table + // of the class file.In retrospect, making 8-byte constants take two + // constant pool entries was a poor choice. + int entry = ++constantPoolEntryCount; + ++constantPoolEntryCount; + return entry; + }); + } + + private int doubleConstant(double value) { + return doubleCache.computeIfAbsent(Double.doubleToRawLongBits(value), + key -> { + constantPool.writeU1(CONSTANT_Double); + constantPool.writeDouble(value); + // All 8-byte constants take up two entries in the constant_pool table + // of the class file.In retrospect, making 8-byte constants take two + // constant pool entries was a poor choice. + int entry = ++constantPoolEntryCount; + ++constantPoolEntryCount; + return entry; + }); + } + + private int intConstant(int value) { + return intCache.computeIfAbsent(value, + key -> { + constantPool.writeU1(CONSTANT_Integer); + constantPool.writeInt(value); + return ++constantPoolEntryCount; + }); + } + + private int floatConstant(float value) { + return floatCache.computeIfAbsent(Float.floatToRawIntBits(value), + key -> { + constantPool.writeU1(CONSTANT_Float); + constantPool.writeFloat(value); + return ++constantPoolEntryCount; + }); + } + + private int string(String str) { + return stringCache.computeIfAbsent(str, + key -> { + int stringIndex = utf8(str); + constantPool.writeU1(CONSTANT_String); + constantPool.writeU2(stringIndex); + return ++constantPoolEntryCount; + }); + } + + private int ldcConstant(Object javaConstantOrType) { + if (javaConstantOrType instanceof JavaConstant) { + JavaConstant javaConstant = (JavaConstant) javaConstantOrType; + // @formatter:off + switch (javaConstant.getJavaKind()) { + case Int : return intConstant(javaConstant.asInt()); + case Float : return floatConstant(javaConstant.asFloat()); + case Long : return longConstant(javaConstant.asLong()); + case Double : return doubleConstant(javaConstant.asDouble()); + case Object : { + Object value = extractConstantValue(javaConstant); + if (value instanceof String) { + return string((String) value); + } else if (value instanceof MethodType) { + return methodType((MethodType) value); + } else { + throw VMError.unimplemented("LDC methodHandle constant"); + } + } + default: + throw VMError.shouldNotReachHereAtRuntime(); + } + // @formatter:on + } else if (javaConstantOrType instanceof JavaType) { + return classConstant((JavaType) javaConstantOrType); + } else { + throw VMError.shouldNotReachHereAtRuntime(); + } + } + + private int nameAndTypeImpl(String name, String descriptor) { + return nameAndTypeCache.computeIfAbsent(Pair.create(name, descriptor), + key -> { + int nameIndex = utf8(name); + int descriptorIndex = utf8(descriptor); + constantPool.writeU1(CONSTANT_NameAndType); + constantPool.writeU2(nameIndex); + constantPool.writeU2(descriptorIndex); + return ++constantPoolEntryCount; + }); + } + + private int nameAndType(String name, JavaType fieldDescriptor) { + String descriptor = fieldDescriptor.getName(); + return nameAndTypeImpl(name, descriptor); + } + + private int nameAndType(String name, Signature methodDescriptor) { + String descriptor = methodDescriptor.toMethodDescriptor(); + return nameAndTypeImpl(name, descriptor); + } + + private int fieldRef(JavaField field) { + return fieldRefCache.computeIfAbsent(MetadataUtil.toUniqueString(field), + key -> { + int classIndex = classConstant(field.getDeclaringClass()); + int nameAndTypeIndex = nameAndType(field.getName(), field.getType()); + constantPool.writeU1(CONSTANT_Fieldref); + constantPool.writeU2(classIndex); + constantPool.writeU2(nameAndTypeIndex); + return ++constantPoolEntryCount; + }); + } + + private int classConstant(JavaType type) { + return classCache.computeIfAbsent(MetadataUtil.toUniqueString(type), + key -> { + int nameIndex = utf8(toConstantPoolName(type)); + constantPool.writeU1(CONSTANT_Class); + constantPool.writeU2(nameIndex); + return ++constantPoolEntryCount; + }); + } + + private int methodRef(JavaMethod method) { + return methodRefCache.computeIfAbsent(MetadataUtil.toUniqueString(method), + key -> { + int classIndex = classConstant(method.getDeclaringClass()); + int nameAndTypeIndex = nameAndType(method.getName(), method.getSignature()); + constantPool.writeU1(CONSTANT_Methodref); + constantPool.writeU2(classIndex); + constantPool.writeU2(nameAndTypeIndex); + return ++constantPoolEntryCount; + }); + } + + private int interfaceMethodRef(JavaMethod method) { + return interfaceMethodRefCache.computeIfAbsent(MetadataUtil.toUniqueString(method), + key -> { + assert !(method instanceof ResolvedJavaMethod) || ((ResolvedJavaMethod) method).getDeclaringClass().isInterface() : method; + int classIndex = classConstant(method.getDeclaringClass()); + int nameAndTypeIndex = nameAndType(method.getName(), method.getSignature()); + constantPool.writeU1(CONSTANT_InterfaceMethodref); + constantPool.writeU2(classIndex); + constantPool.writeU2(nameAndTypeIndex); + return ++constantPoolEntryCount; + }); + } + + @SuppressWarnings("unused") + private int module(String moduleName) { + return moduleCache.computeIfAbsent(moduleName, + key -> { + int nameIndex = utf8(moduleName); + constantPool.writeU1(CONSTANT_Module); // u1 tag + constantPool.writeU2(nameIndex); // u2 name_index; + return ++constantPoolEntryCount; + }); + } + + @SuppressWarnings("unused") + private int packageConstant(String packageName) { + return packageCache.computeIfAbsent(packageName, + key -> { + int nameIndex = utf8(packageName); + constantPool.writeU1(CONSTANT_Package); + constantPool.writeU2(nameIndex); + return ++constantPoolEntryCount; + }); + } + + private int methodType(MethodType methodType) { + String descriptor = methodType.descriptorString(); + return methodTypeCache.computeIfAbsent(descriptor, + key -> { + int descriptorIndex = utf8(descriptor); + constantPool.writeU1(CONSTANT_MethodType); + constantPool.writeU2(descriptorIndex); + return ++constantPoolEntryCount; + }); + } + + @SuppressWarnings("unused") public static final byte REF_NONE = 0; // null value + public static final byte REF_getField = 1; + public static final byte REF_getStatic = 2; + public static final byte REF_putField = 3; + public static final byte REF_putStatic = 4; + public static final byte REF_invokeVirtual = 5; + public static final byte REF_invokeStatic = 6; + public static final byte REF_invokeSpecial = 7; + public static final byte REF_newInvokeSpecial = 8; + public static final byte REF_invokeInterface = 9; + + @SuppressWarnings("unused") public static final byte REF_LIMIT = 10; + + @SuppressWarnings("unused") + private int methodHandle(byte referenceKind, JavaField referenceField) { + VMError.guarantee(referenceKind == REF_getField || referenceKind == REF_getStatic || referenceKind == REF_putField || referenceKind == REF_putStatic); + return methodHandleCache.computeIfAbsent(Pair.create(referenceKind, referenceField), + key -> { + int referenceIndex = fieldRef(referenceField); + constantPool.writeU1(CONSTANT_MethodHandle); + constantPool.writeU1(referenceKind); + constantPool.writeU2(referenceIndex); + return ++constantPoolEntryCount; + }); + } + + @SuppressWarnings("unused") + private int methodHandle(byte referenceKind, ResolvedJavaMethod referenceMethod) { + VMError.guarantee(referenceKind == REF_invokeVirtual || referenceKind == REF_invokeStatic || referenceKind == REF_invokeSpecial || referenceKind == REF_newInvokeSpecial || + referenceKind == REF_invokeInterface); + return methodHandleCache.computeIfAbsent(Pair.create(referenceKind, referenceMethod), + key -> { + int referenceIndex; + switch (referenceKind) { + case REF_invokeVirtual: // fall-through + case REF_newInvokeSpecial: + referenceIndex = methodRef(referenceMethod); + break; + case REF_invokeStatic: // fall-through + case REF_invokeSpecial: + // GR-55048: Java version check, interfaceMethodRef + // allowed after >= 52 + if (referenceMethod.getDeclaringClass().isInterface()) { + referenceIndex = interfaceMethodRef(referenceMethod); + } else { + referenceIndex = methodRef(referenceMethod); + } + break; + case REF_invokeInterface: + referenceIndex = interfaceMethodRef(referenceMethod); + break; + default: + throw VMError.shouldNotReachHere("invalid methodHandle ref kind"); + } + constantPool.writeU1(CONSTANT_MethodHandle); + constantPool.writeU1(referenceKind); + constantPool.writeU2(referenceIndex); + return ++constantPoolEntryCount; + }); + } + + private final InterpreterUniverse universe; + private final Function extractConstantValue; + + private Object extractConstantValue(Object constant) { + return extractConstantValue.apply(constant); + } + + //@formatter:off + private static final Function EXTRACT_INTERPRETER_CONSTANT_VALUE = (constant) -> { + if (constant instanceof JavaType) { + return constant; + } + if (constant instanceof PrimitiveConstant primitiveConstant) { + return primitiveConstant.asBoxedPrimitive(); + } + if (constant instanceof ReferenceConstant) { + return ((ReferenceConstant) constant).getReferent(); + } + throw VMError.shouldNotReachHere("unexpected constant"); + }; + //@formatter:on + + private ResolvedJavaType getSuperclass(ResolvedJavaType type) { + if (type instanceof InterpreterResolvedJavaType interpreterResolvedJavaType) { + Class superclass = interpreterResolvedJavaType.getJavaClass().getSuperclass(); + if (superclass == null) { + return null; + } + return universe.lookupType(superclass); + } else { + return type.getSuperclass(); + } + } + + private ResolvedJavaType[] getInterfaces(ResolvedJavaType type) { + if (type instanceof InterpreterResolvedJavaType interpreterResolvedJavaType) { + Class[] interfaces = interpreterResolvedJavaType.getJavaClass().getInterfaces(); + ResolvedJavaType[] result = new InterpreterResolvedObjectType[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + result[i] = universe.lookupType(interfaces[i]); + } + return result; + } else { + return type.getInterfaces(); + } + } + + private ResolvedJavaMethod getClassInitializer(ResolvedJavaType type) { + if (type instanceof InterpreterResolvedJavaType) { + return universe.getAllDeclaredMethods(type) + .stream() + .filter(ResolvedJavaMethod::isClassInitializer) + .findAny() + .orElse(null); + } else { + return type.getClassInitializer(); + } + } + + private ResolvedJavaMethod[] getDeclaredConstructors(ResolvedJavaType type) { + if (type instanceof InterpreterResolvedJavaType) { + return universe.getAllDeclaredMethods(type) + .stream() + .filter(ResolvedJavaMethod::isConstructor) + .toArray(InterpreterResolvedJavaMethod[]::new); + } else { + return type.getDeclaredConstructors(); + } + } + + private ResolvedJavaMethod[] getDeclaredMethods(ResolvedJavaType type) { + if (type instanceof InterpreterResolvedJavaType) { + return universe.getAllDeclaredMethods(type) + .stream() + .filter(method -> !method.isConstructor() && !method.isClassInitializer()) + .toArray(ResolvedJavaMethod[]::new); + } else { + return type.getDeclaredMethods(); + } + } + + private ResolvedJavaField[] getInstanceFields(ResolvedJavaType type, boolean includeSuperclasses) { + if (type instanceof InterpreterResolvedJavaType) { + if (includeSuperclasses) { + throw VMError.unimplemented("getInstanceFields with includeSuperclasses=true"); + } + return universe.getAllDeclaredFields(type) + .stream() + .filter(f -> !f.isStatic()) + .toArray(ResolvedJavaField[]::new); + } else { + return type.getInstanceFields(includeSuperclasses); + } + } + + private ResolvedJavaField[] getStaticFields(ResolvedJavaType type) { + if (type instanceof InterpreterResolvedJavaType) { + return universe.getAllDeclaredFields(type) + .stream() + .filter(ModifiersProvider::isStatic) + .toArray(ResolvedJavaField[]::new); + } else { + return type.getStaticFields(); + } + } + + static byte[] dumpResolvedJavaTypeClassFile(InterpreterUniverse universe, ResolvedJavaType type, Function extractConstantValue) { + VMError.guarantee(!type.isPrimitive() && !type.isArray()); + + ClassFile cf = new ClassFile(universe, extractConstantValue); + cf.dumpClassFileImpl(type); + OutStream ensemble = new OutStream(); + + // Header + ensemble.writeInt(MAGIC); + ensemble.writeU2(MINOR_VERSION); + ensemble.writeU2(MAJOR_VERSION); + // Constant pool + // The value of the constant_pool_count item is equal to the number of entries in the + // constant_pool table plus one. + ensemble.writeU2(cf.constantPoolEntryCount + 1); + ensemble.writeBytes(cf.constantPool.toArray()); + // Tail + ensemble.writeBytes(cf.classFile.toArray()); + + return ensemble.toArray(); + } + + public static byte[] dumpInterpreterTypeClassFile(InterpreterUniverse universe, InterpreterResolvedJavaType type) { + return dumpResolvedJavaTypeClassFile(universe, type, EXTRACT_INTERPRETER_CONSTANT_VALUE); + } + + void dumpSourceFileAttribute(String sourceFileName) { + if (sourceFileName == null) { + return; + } + // SourceFile_attribute { + // u2 attribute_name_index; + // u4 attribute_length; + // u2 sourcefile_index; + // } + classFile.writeU2(utf8("SourceFile")); + classFile.writeInt(2); + classFile.writeU2(utf8(sourceFileName)); + } + + void dumpClassFileImpl(ResolvedJavaType type) { + // ClassFile { + // u4 magic; + // u2 minor_version; + // u2 major_version; + // u2 constant_pool_count; + // cp_info constant_pool[constant_pool_count-1]; + + // Constant pool is computed separately during dumping, dumping starts here: + + // u2 access_flags; + // u2 this_class; + // u2 super_class; + // u2 interfaces_count; + // u2 interfaces[interfaces_count]; + // u2 fields_count; + // field_info fields[fields_count]; + // u2 methods_count; + // method_info methods[methods_count]; + // u2 attributes_count; + // attribute_info attributes[attributes_count]; + // } + + List allDeclaredMethods = new ArrayList<>(); + + // Static initializers are not included for classes already initialized. + if (getClassInitializer(type) != null) { + allDeclaredMethods.add(getClassInitializer(type)); + } + allDeclaredMethods.addAll(Arrays.asList(getDeclaredConstructors(type))); + allDeclaredMethods.addAll(Arrays.asList(getDeclaredMethods(type))); + + // Write all 1-byte CPIs first in the constant pool. + processLDC(allDeclaredMethods); + // processINVOKEDYNAMIC(allDeclaredMethods); + + // u2 access_flags; + classFile.writeU2(type.getModifiers() & Modifier.classModifiers()); + + // u2 this_class; + classFile.writeU2(classConstant(type)); // GR-55048: Handle hidden classes. + + // u2 super_class; + if (getSuperclass(type) != null) { + classFile.writeU2(classConstant(getSuperclass(type))); + } else { + classFile.writeU2(0); // no super class + } + + // u2 interfaces_count; + classFile.writeU2(getInterfaces(type).length); + + // u2 interfaces[interfaces_count]; + for (JavaType i : getInterfaces(type)) { + classFile.writeU2(classConstant(i)); + } + + List fields = new ArrayList<>(); + fields.addAll(Arrays.asList(getStaticFields(type))); + fields.addAll(Arrays.asList(getInstanceFields(type, false))); + // u2 fields_count; + classFile.writeU2(fields.size()); + + // field_info fields[fields_count]; + for (ResolvedJavaField f : fields) { + dumpFieldInfo(f); + } + + // u2 methods_count; + classFile.writeU2(allDeclaredMethods.size()); + + // method_info methods[methods_count]; + for (ResolvedJavaMethod m : allDeclaredMethods) { + dumpMethodInfo(m); + } + + int attributeCount = 0; + if (getSourceFileName(type) != null) { + attributeCount++; + } + + // u2 attributes_count; + classFile.writeU2(attributeCount); + + // attribute_info attributes[attributes_count]; + if (getSourceFileName(type) != null) { + dumpSourceFileAttribute(getSourceFileName(type)); + } + + // SourceFile + // InnerClasses + // EnclosingMethod + // SourceDebugExtension + // BootstrapMethods + // Module + // ModulePackages + // ModuleMainClass + // NestHost + // NestMembers + // Record + // PermittedSubclasses + } + + private static String getSourceFileName(ResolvedJavaType type) { + if (type instanceof InterpreterResolvedJavaType) { + return ((InterpreterResolvedObjectType) type).getOriginalType().getSourceFileName(); + } else { + return type.getSourceFileName(); + } + } + + final class ConstantWrapper { + final Object constant; + + ConstantWrapper(Object constant) { + this.constant = constant; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ConstantWrapper)) { + return false; + } + ConstantWrapper that = (ConstantWrapper) other; + Object thisValue = extractConstantValue(this.constant); + Object thatValue = extractConstantValue(that.constant); + // Compare types by name. + if (thisValue instanceof JavaType) { + return (thatValue instanceof JavaType) && + ((JavaType) thisValue).getName().equals(((JavaType) thatValue).getName()); + } + return thisValue.equals(thatValue); + } + + @Override + public int hashCode() { + Object value = extractConstantValue(this.constant); + if (value instanceof JavaType) { + return ((JavaType) value).getName().hashCode(); + } + return value.hashCode(); + } + } + + private void processLDC(List methods) { + Set pending = new HashSet<>(); + + for (ResolvedJavaMethod m : methods) { + if (!m.hasBytecodes()) { + continue; + } + byte[] code = m.getCode(); + if (code == null || code.length == 0) { + continue; + } + for (int bci = 0; bci < BytecodeStream.endBCI(code); bci = BytecodeStream.nextBCI(code, bci)) { + if (BytecodeStream.opcode(code, bci) == LDC) { + int originalCPI = BytecodeStream.readCPI(code, bci); + if (originalCPI != 0) { + Object constant = m.getConstantPool().lookupConstant(originalCPI); + if (constant instanceof PrimitiveConstant) { + ldcConstant(constant); // push it on the constant pool + } else { + // Constant can be String, MethodType and MethodHandle. + pending.add(new ConstantWrapper(constant)); + } + } + } + } + } + + /* + * Class, String and MethodType constants are pointers to UTF8 entries. LDC doesn't refer to + * UTF8 entries directly, but these UTF8 entries may needlessly occupy slots of the 255 + * addressable by LDC, running out of addressable slots for the constants. Constant + * dependencies are written always before in the constant pool, but in this case UTF8 + * entries must be added after to ensure the constant indices remain addressable by LDC. + */ + List newEntries = new ArrayList<>(); + for (ConstantWrapper wrapper : pending) { + Object constant = wrapper.constant; + Object value = extractConstantValue(constant); + String utf8Entry = null; + if (value instanceof JavaType) { + utf8Entry = toConstantPoolName((JavaType) value); + } else if (value instanceof String) { + utf8Entry = (String) value; + } else if (value instanceof MethodType) { + MethodType methodType = (MethodType) value; + utf8Entry = methodType.toMethodDescriptorString(); + } else { + // MethodHandle is not implemented, never seen one either. + throw VMError.unimplemented("LDC methodHandle constant"); + } + VMError.guarantee(utf8Entry != null); + if (!utf8Cache.containsKey(utf8Entry)) { + newEntries.add(utf8Entry); + utf8Cache.put(utf8Entry, constantPoolEntryCount + pending.size() + newEntries.size()); + } + } + + int start = constantPoolEntryCount; + for (ConstantWrapper wrapper : pending) { + ldcConstant(wrapper.constant); + } + int end = constantPoolEntryCount; + VMError.guarantee(end - start == pending.size()); + for (String entry : newEntries) { + constantPool.writeU1(CONSTANT_Utf8); + constantPool.writeUTF(entry); + ++constantPoolEntryCount; + } + } + + private static String toConstantPoolName(JavaType type) { + return type.toJavaName().replace('.', '/'); + } + + private void dumpFieldInfo(ResolvedJavaField field) { + int accessFlags = field.getModifiers() & Modifier.fieldModifiers(); + int nameIndex = utf8(field.getName()); + int descriptorIndex = utf8(field.getType().getName()); + + // field_info { + // u2 access_flags; + // u2 name_index; + // u2 descriptor_index; + // u2 attributes_count; + // attribute_info attributes[attributes_count]; + // } + + classFile.writeU2(accessFlags); + classFile.writeU2(nameIndex); + classFile.writeU2(descriptorIndex); + + int attributeCount = 0; + classFile.writeU2(attributeCount); + + // Field attributes: + // ConstantValue + // Synthetic + // Deprecated + // Signature + // RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations + // RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations + } + + private void dumpMethodInfo(ResolvedJavaMethod method) { + int accessFlags = method.getModifiers() & Modifier.methodModifiers(); + int nameIndex = utf8(method.getName()); + int descriptorIndex = utf8(method.getSignature().toMethodDescriptor()); + + // method_info { + // u2 access_flags; + // u2 name_index; + // u2 descriptor_index; + // u2 attributes_count; + // attribute_info attributes[attributes_count]; + // } + + classFile.writeU2(accessFlags); + classFile.writeU2(nameIndex); + classFile.writeU2(descriptorIndex); + + int attributeCount = 0; + if (method.hasBytecodes()) { + ++attributeCount; + } + + ResolvedJavaMethod.Parameter[] methodParameters = method.getParameters(); + if (methodParameters != null) { + ++attributeCount; + } + + classFile.writeU2(attributeCount); + if (method.hasBytecodes()) { + dumpCodeAttribute(method); + } + + if (methodParameters != null) { + dumpMethodParameters(methodParameters); + } + + // Method attributes: + // Code + // Exceptions + // RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations + // AnnotationDefault + // MethodParameters + // Synthetic + // Deprecated + // Signature + // RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations + // RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations + } + + private void dumpMethodParameters(ResolvedJavaMethod.Parameter[] methodParameters) { + if (methodParameters == null) { + return; + } + + // MethodParameters_attribute { + // u2 attribute_name_index; + // u4 attribute_length; + // u1 parameters_count; + // { u2 name_index; + // u2 access_flags; + // } parameters[parameters_count]; + // } + + classFile.writeU2(utf8("MethodParameters")); + int attributeLength = 1 + methodParameters.length * 4; + classFile.writeInt(attributeLength); + + classFile.writeU1(attributeLength); + for (ResolvedJavaMethod.Parameter p : methodParameters) { + classFile.writeU2(utf8(p.getName())); + classFile.writeU2(p.getModifiers()); + } + } + + private void dumpLineNumberTable(LineNumberTable lineNumberTable) { + if (lineNumberTable == null) { + return; + } + + // LineNumberTable_attribute { + // u2 attribute_name_index; + // u4 attribute_length; + // u2 line_number_table_length; + // { u2 start_pc; + // u2 line_number; + // } line_number_table[line_number_table_length]; + // } + + classFile.writeU2(utf8("LineNumberTable")); + + int[] lineNumbers = lineNumberTable.getLineNumbers(); + int[] bcis = lineNumberTable.getBcis(); + + assert lineNumbers.length == bcis.length; + + int entryCount = lineNumbers.length; + + int attributeLength = 2 + entryCount * 4; + classFile.writeInt(attributeLength); + + classFile.writeU2(entryCount); + for (int i = 0; i < entryCount; ++i) { + classFile.writeU2(bcis[i]); + classFile.writeU2(lineNumbers[i]); + } + } + + private void dumpLocalVariableTable(LocalVariableTable localVariableTable) { + if (localVariableTable == null) { + return; + } + + // LocalVariableTable_attribute { + // u2 attribute_name_index; + // u4 attribute_length; + // u2 local_variable_table_length; + // { u2 start_pc; + // u2 length; + // u2 name_index; + // u2 descriptor_index; + // u2 index; + // } local_variable_table[local_variable_table_length]; + // } + + classFile.writeU2(utf8("LocalVariableTable")); + Local[] locals = localVariableTable.getLocals(); + + int attributeLength = 2 + locals.length * 10; + classFile.writeInt(attributeLength); + + classFile.writeU2(locals.length); + for (Local local : locals) { + classFile.writeU2(local.getStartBCI()); + classFile.writeU2(local.getEndBCI() - local.getStartBCI()); + classFile.writeU2(utf8(local.getName())); + JavaType type = local.getType(); + if (type == null) { + classFile.writeU2(0); + } else { + classFile.writeU2(utf8(type.getName())); + } + classFile.writeU2(local.getSlot()); + } + } + + private void dumpCodeAttribute(ResolvedJavaMethod method) { + // Code_attribute { + // u2 attribute_name_index; + // u4 attribute_length; + // u2 max_stack; + // u2 max_locals; + // u4 code_length; + // u1 code[code_length]; + // u2 exception_table_length; + // { u2 start_pc; + // u2 end_pc; + // u2 handler_pc; + // u2 catch_type; + // } exception_table[exception_table_length]; + // u2 attributes_count; + // attribute_info attributes[attributes_count]; + // } + + int startOffset = classFile.getOffset(); + classFile.writeU2(utf8("Code")); + classFile.writeInt(0xDEADBEEF); // placeholder + + classFile.writeU2(method.getMaxStackSize()); + classFile.writeU2(method.getMaxLocals()); + + byte[] code = method.getCode(); + if (code == null) { + classFile.writeInt(0); + // empty + } else { + classFile.writeInt(code.length); + classFile.writeBytes(recomputeConstantPoolIndices(method)); + } + + ExceptionHandler[] handlers = method.getExceptionHandlers(); + if (handlers == null || handlers.length == 0) { + classFile.writeU2(0); + // empty + } else { + classFile.writeU2(handlers.length); + for (ExceptionHandler eh : handlers) { + classFile.writeU2(eh.getStartBCI()); + classFile.writeU2(eh.getEndBCI()); + classFile.writeU2(eh.getHandlerBCI()); + classFile.writeU2(eh.catchTypeCPI()); + } + } + + int attributeCount = 0; + LineNumberTable lineNumberTable = method.getLineNumberTable(); + if (lineNumberTable != null) { + ++attributeCount; + } + LocalVariableTable localVariableTable = method.getLocalVariableTable(); + if (localVariableTable != null) { + ++attributeCount; + } + + classFile.writeU2(attributeCount); + + if (lineNumberTable != null) { + dumpLineNumberTable(lineNumberTable); + } + if (localVariableTable != null) { + dumpLocalVariableTable(localVariableTable); + } + + // Code attributes: + // LineNumberTable + // LocalVariableTable + // LocalVariableTypeTable + // StackMapTable + + int attributeLength = classFile.getOffset() - startOffset - 6; + classFile.patchAtOffset(startOffset + 2, () -> classFile.writeInt(attributeLength)); + } + + private byte[] recomputeConstantPoolIndices(ResolvedJavaMethod method) { + byte[] originalCode = method.getCode(); + if (originalCode == null || originalCode.length == 0) { + return originalCode; + } + + byte[] code = originalCode.clone(); + ConstantPool originalConstantPool = method.getConstantPool(); + + for (int bci = 0; bci < BytecodeStream.endBCI(code); bci = BytecodeStream.nextBCI(code, bci)) { + int bytecode = BytecodeStream.currentBC(code, bci); // also handles wide bytecodes + // @formatter:off + switch (bytecode) { + case CHECKCAST : // fall-through + case INSTANCEOF : // fall-through + case NEW : // fall-through + case ANEWARRAY : // fall-through + case MULTIANEWARRAY: { + int originalCPI = BytecodeStream.readCPI(code, bci); + int newCPI = 0; + if (originalCPI != 0) { + newCPI = classConstant(originalConstantPool.lookupType(originalCPI, bytecode)); + } + BytecodeStream.patchCPI(code, bci, newCPI); + break; + } + case LDC : // fall-through + case LDC_W : // fall-through + case LDC2_W: { + int originalCPI = BytecodeStream.readCPI(code, bci); + int newCPI = 0; + if (originalCPI != 0) { + Object constant = originalConstantPool.lookupConstant(BytecodeStream.readCPI(code, bci)); + newCPI = ldcConstant(constant); + } + BytecodeStream.patchCPI(code, bci, newCPI); + break; + } + case GETSTATIC : // fall-through + case PUTSTATIC : // fall-through + case GETFIELD : // fall-through + case PUTFIELD: { + int originalCPI = BytecodeStream.readCPI(code, bci); + int newCPI = 0; + if (originalCPI != 0) { + newCPI = fieldRef(originalConstantPool.lookupField(originalCPI, method, bytecode)); + } + BytecodeStream.patchCPI(code, bci, newCPI); + break; + } + + case INVOKEVIRTUAL : // fall-through + case INVOKESPECIAL : // fall-through + case INVOKESTATIC : // fall-through + case INVOKEINTERFACE: { + int originalCPI = BytecodeStream.readCPI(code, bci); + int newCPI = 0; + if (originalCPI != 0) { + newCPI = methodRef(originalConstantPool.lookupMethod(originalCPI, bytecode)); + } + BytecodeStream.patchCPI(code, bci, newCPI); + break; + } + + case INVOKEDYNAMIC: + // GR-55048: Cannot derive BootstrapMethods attribute and (Invoke)Dynamic CP entry. + // The VM already provides the resolved bootstrap method and appendix. + // Investigate how to persist the appendix (arbitrary object) on the constant pool. + BytecodeStream.patchCPI(code, bci, 0); + BytecodeStream.patchAppendixCPI(code, bci, 0); + break; + } + // @formatter:on + } + + return code; + } + +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/classfile/OutStream.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/classfile/OutStream.java new file mode 100644 index 000000000000..530b057eaeb3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/classfile/OutStream.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.interpreter.classfile; + +import java.io.UTFDataFormatException; +import java.io.UncheckedIOException; +import java.util.Arrays; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.util.VMError; + +@Platforms(Platform.HOSTED_ONLY.class) +final class OutStream { + + OutStream(int initialCapacity) { + this.bytes = new byte[initialCapacity]; + this.offset = 0; + } + + OutStream() { + this(32); + } + + private byte[] bytes; + private int offset; + + public void writeByte(byte value) { + ensureCapacity(offset + 1); + bytes[offset++] = value; + } + + public void writeU1(int value) { + VMError.guarantee(0 <= value && value <= 0xFF); + ensureCapacity(offset + 1); + bytes[offset++] = (byte) value; + } + + private void ensureCapacity(int capacity) { + if (bytes.length < capacity) { + int newCapacity = Math.max(capacity, bytes.length * 2 + 1); + this.bytes = Arrays.copyOf(bytes, newCapacity); + } + } + + public void writeShort(short value) { + writeByte((byte) (value >>> 8)); + writeByte((byte) value); + } + + public void writeU2(int value) { + VMError.guarantee(0 <= value && value <= 0xFFFF); + writeByte((byte) (value >>> 8)); + writeByte((byte) value); + } + + public void writeInt(int value) { + writeByte((byte) (value >>> 24)); + writeByte((byte) (value >>> 16)); + writeByte((byte) (value >>> 8)); + writeByte((byte) (value >>> 0)); + } + + public void writeLong(long value) { + writeByte((byte) (value >>> 56)); + writeByte((byte) (value >>> 48)); + writeByte((byte) (value >>> 40)); + writeByte((byte) (value >>> 32)); + writeByte((byte) (value >>> 24)); + writeByte((byte) (value >>> 16)); + writeByte((byte) (value >>> 8)); + writeByte((byte) (value >>> 0)); + } + + public void writeFloat(float value) { + writeInt(Float.floatToIntBits(value)); + } + + public void writeDouble(double value) { + writeLong(Double.doubleToLongBits(value)); + } + + public int writeUTF(String str) { + final int strlen = str.length(); + int utflen = strlen; // optimized for ASCII + + for (int i = 0; i < strlen; i++) { + int c = str.charAt(i); + if (c >= 0x80 || c == 0) { + utflen += (c >= 0x800) ? 2 : 1; + } + } + + if (utflen > 65535 || /* overflow */ utflen < strlen) { + throw new UncheckedIOException(new UTFDataFormatException(tooLongMsg(str, utflen))); + } + + writeByte((byte) ((utflen >>> 8) & 0xFF)); + writeByte((byte) ((utflen >>> 0) & 0xFF)); + + int i = 0; + for (i = 0; i < strlen; i++) { // optimized for initial run of ASCII + int c = str.charAt(i); + if (c >= 0x80 || c == 0) { + break; + } + writeByte((byte) c); + } + + for (; i < strlen; i++) { + int c = str.charAt(i); + if (c < 0x80 && c != 0) { + writeByte((byte) c); + } else if (c >= 0x800) { + writeByte((byte) (0xE0 | ((c >> 12) & 0x0F))); + writeByte((byte) (0x80 | ((c >> 6) & 0x3F))); + writeByte((byte) (0x80 | ((c >> 0) & 0x3F))); + } else { + writeByte((byte) (0xC0 | ((c >> 6) & 0x1F))); + writeByte((byte) (0x80 | ((c >> 0) & 0x3F))); + } + } + + return utflen + 2; + } + + private static String tooLongMsg(String s, int bits32) { + int slen = s.length(); + String head = s.substring(0, 8); + String tail = s.substring(slen - 8, slen); + // handle int overflow with max 3x expansion + long actualLength = slen + Integer.toUnsignedLong(bits32 - slen); + return "encoded string (" + head + "..." + tail + ") too long: " + actualLength + " bytes"; + } + + byte[] toArray() { + return Arrays.copyOf(bytes, offset); + } + + public void writeBytes(byte[] byteArray) { + for (byte b : byteArray) { + writeByte(b); + } + } + + int getOffset() { + return offset; + } + + void patchAtOffset(int targetOffset, Runnable action) { + int oldOffset = getOffset(); + try { + this.offset = targetOffset; + action.run(); + } finally { + this.offset = oldOffset; + } + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/Bits.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/Bits.java new file mode 100644 index 000000000000..10ef48d0039a --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/Bits.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +final class Bits { + public static int setBit(int n, int bitIndex, boolean value) { + if (value) { + return n | (1 << bitIndex); + } else { + return n & ~(1 << bitIndex); + } + } + + public static boolean testBit(int n, int bitIndex) { + return (n & (1 << bitIndex)) != 0; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEvents.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEvents.java new file mode 100644 index 000000000000..be9a43973901 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEvents.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +public interface DebuggerEvents { + + /** + * Enable/disable events in a specific thread or globally. An event is generated for a + * particular thread if it is enabled either at the thread or global levels. + * + * @param thread thread where the event is enabled/disabled, or{@code null} to specify global + * scope (all threads) + * @param eventKind one of the events in {@link EventKind} + * @param enable true/false to enable/disable the event + */ + void setEventEnabled(Thread thread, EventKind eventKind, boolean enable); + + /** + * Checks if an event is enabled for a specified thread or globally. An event is generated + * for a particular thread if it is enabled either at the thread or global levels. + * + * @param thread where the event is enabled/disabled, or{@code null} to specify global * scope + * (all threads) + * @param eventKind one of the events in {@link EventKind} + */ + boolean isEventEnabled(Thread thread, EventKind eventKind); + + /** + * Enable/disable a breakpoint on the specified method and bytecode index. + * + * @throws IllegalArgumentException if the method doesn't have bytecodes, or the bytecode index + * is out of range, or if the bytecode index is not a valid b + */ + void toggleBreakpoint(ResolvedJavaMethod method, int bci, boolean enable); + + void toggleMethodEnterEvent(ResolvedJavaType clazz, boolean enable); + + void toggleMethodExitEvent(ResolvedJavaType clazz, boolean enable); + + /** + * Sets the {@link SteppingControl stepping information} associated with a thread. This enables + * stepping for the specified thread, enabling stepping is not enough, the stepping events must + * be enabled e.g. {@code Debugger.setEventEnabled(GLOBAL|threadId, EventKind.SINGLE_STEP, true} + * + * For line-stepping, the {@link SteppingControl#setStartingLocation(Location) starting location + * can be set}, otherwise, any location will raise the stepping event. + */ + void setSteppingFromLocation(Thread thread, int depth, int size, Location location); + + default void setStepping(Thread thread, int depth, int size) { + setSteppingFromLocation(thread, depth, size, null); + } + + /** + * Removes the {@link SteppingControl stepping information} associated with a thread. This + * cancels stepping for the current thread. + */ + void clearStepping(Thread thread); + + void setEventHandler(EventHandler eventHandler); + + EventHandler getEventHandler(); + + /** + * Returns the {@link SteppingControl stepping information} associated with a thread. + */ + SteppingControl getSteppingControl(Thread thread); +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEventsFeature.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEventsFeature.java new file mode 100644 index 000000000000..76681c21c576 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEventsFeature.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.option.HostedOptionKey; + +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionType; + +@Platforms(Platform.HOSTED_ONLY.class) +public final class DebuggerEventsFeature implements InternalFeature { + + public static final class DebuggerOptions { + @Option(help = "Enables experimental support for debugger events.", type = OptionType.Expert)// + public static final HostedOptionKey DebuggerEvents = new HostedOptionKey<>(false); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + DebuggerEvents impl; + if (DebuggerOptions.DebuggerEvents.getValue()) { + impl = new DebuggerEventsImpl(); + } else { + impl = new DummyDebuggerEventsImpl(); + } + ImageSingletons.add(DebuggerEvents.class, impl); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEventsImpl.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEventsImpl.java new file mode 100644 index 000000000000..08de5af21358 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DebuggerEventsImpl.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +import com.oracle.svm.interpreter.DebuggerSupport; +import com.oracle.svm.interpreter.InterpreterUtil; +import jdk.graal.compiler.core.common.SuppressFBWarnings; +import org.graalvm.nativeimage.IsolateThread; + +import com.oracle.svm.interpreter.InterpreterDirectives; +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; +import com.oracle.svm.core.thread.PlatformThreads; +import com.oracle.svm.core.threadlocal.FastThreadLocalFactory; +import com.oracle.svm.core.threadlocal.FastThreadLocalInt; +import com.oracle.svm.core.threadlocal.FastThreadLocalObject; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; + +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +public final class DebuggerEventsImpl implements DebuggerEvents { + + private static int globalEnabledEventsMask; + + private static final FastThreadLocalInt perThreadEnabledEventsMask = FastThreadLocalFactory.createInt("Debugger.perThreadEnabledEventsMask"); + + private static final FastThreadLocalObject perThreadSteppingControl = FastThreadLocalFactory.createObject(SteppingControl.class, "Debugger.perThreadSteppingControl"); + + private static EventHandler eventHandler; + + @Override + public void setEventEnabled(Thread thread, EventKind eventKind, boolean enable) { + if (thread == null) { + globalEnabledEventsMask = Bits.setBit(globalEnabledEventsMask, eventKind.ordinal(), enable); + } else { + IsolateThread isolateThread = PlatformThreads.getIsolateThreadUnsafe(thread); + int oldMask; + int newMask; + do { + oldMask = perThreadEnabledEventsMask.getVolatile(isolateThread); + newMask = Bits.setBit(oldMask, eventKind.ordinal(), enable); + } while (!perThreadEnabledEventsMask.compareAndSet(isolateThread, oldMask, newMask)); + } + } + + /** + * Returns events enabled for the specified thread, including the globally enabled events. If + * the thread is null, then returns events enabled globally. + */ + private static int enabledEventsMask(Thread thread) { + if (thread == null) { + return globalEnabledEventsMask; + } else { + IsolateThread isolateThread = PlatformThreads.getIsolateThreadUnsafe(thread); + return (globalEnabledEventsMask | perThreadEnabledEventsMask.get(isolateThread)); + } + } + + @Override + public boolean isEventEnabled(Thread thread, EventKind eventKind) { + return Bits.testBit(enabledEventsMask(thread), eventKind.ordinal()); + } + + @Override + @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "Intentional.") + public void setEventHandler(EventHandler eventHandler) { + DebuggerEventsImpl.eventHandler = eventHandler; + } + + @Override + public EventHandler getEventHandler() { + return eventHandler; + } + + @Override + public void toggleBreakpoint(ResolvedJavaMethod method, int bci, boolean enable) { + InterpreterResolvedJavaMethod interpreterMethod = (InterpreterResolvedJavaMethod) method; + interpreterMethod.ensureCanSetBreakpointAt(bci); + interpreterMethod.toggleBreakpoint(bci, enable); + InterpreterUtil.traceInterpreter(enable ? "Setting" : "Unsetting") + .string(" breakpoint for method=") + .string(interpreterMethod.toString()) + .string(" at bci=").signed(bci) + .newline(); + if (enable) { + // GR-54095: Make method and all the callers that inline it run in the + // interpreter. + // This operation cannot be nested, methods need to keep counters. + // Ignore token for now. + InterpreterDirectives.ensureInterpreterExecution(interpreterMethod); + } + } + + @Override + public void toggleMethodEnterEvent(ResolvedJavaType clazz, boolean enable) { + toggleDeclaredMethodsInterpreterExecution(clazz, enable); + ((InterpreterResolvedJavaType) clazz).toggleMethodEnterEvent(enable); + } + + @Override + public void toggleMethodExitEvent(ResolvedJavaType clazz, boolean enable) { + toggleDeclaredMethodsInterpreterExecution(clazz, enable); + ((InterpreterResolvedJavaType) clazz).toggleMethodExitEvent(enable); + } + + private static void toggleDeclaredMethodsInterpreterExecution(ResolvedJavaType clazz, boolean enable) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + for (ResolvedJavaMethod rMethod : universe.getAllDeclaredMethods(clazz)) { + InterpreterResolvedJavaMethod method = (InterpreterResolvedJavaMethod) rMethod; + Object token = method.getInterpreterExecToken(); + if (enable) { + if (token == null) { + token = InterpreterDirectives.ensureInterpreterExecution(method); + method.setInterpreterExecToken(token); + } + } else if (token != null) { + // GR-54095: The undoExecutionOperation() can not be nested + // InterpreterDirectives.undoExecutionOperation(token); + method.setInterpreterExecToken(null); + } + } + } + + @Override + public SteppingControl getSteppingControl(Thread thread) { + VMError.guarantee(thread != null); + + IsolateThread isolateThread = PlatformThreads.getIsolateThreadUnsafe(thread); + return perThreadSteppingControl.get(isolateThread); + } + + @Override + public void setSteppingFromLocation(Thread thread, int depth, int size, Location location) { + VMError.guarantee(thread != null); + + IsolateThread isolateThread = PlatformThreads.getIsolateThreadUnsafe(thread); + SteppingControl value = new SteppingControl(thread, depth, size); + perThreadSteppingControl.set(isolateThread, value); + + if (location != null) { + value.setStartingLocation(location); + } + } + + @Override + public void clearStepping(Thread thread) { + VMError.guarantee(thread != null); + IsolateThread isolateThread = PlatformThreads.getIsolateThreadUnsafe(thread); + perThreadSteppingControl.set(isolateThread, null); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DummyDebuggerEventsImpl.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DummyDebuggerEventsImpl.java new file mode 100644 index 000000000000..bfb218823303 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/DummyDebuggerEventsImpl.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.Interpreter; + +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * Allows the {@link Interpreter} to run without any debugger hooks. + */ +final class DummyDebuggerEventsImpl implements DebuggerEvents { + @Override + public void setEventEnabled(Thread thread, EventKind eventKind, boolean enable) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public boolean isEventEnabled(Thread thread, EventKind eventKind) { + return false; + } + + @Override + public void setEventHandler(EventHandler eventHandler) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public EventHandler getEventHandler() { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public void toggleBreakpoint(ResolvedJavaMethod method, int bci, boolean enable) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public void toggleMethodEnterEvent(ResolvedJavaType clazz, boolean enable) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public void toggleMethodExitEvent(ResolvedJavaType clazz, boolean enable) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public SteppingControl getSteppingControl(Thread thread) { + return null; + } + + @Override + public void setSteppingFromLocation(Thread thread, int depth, int size, Location location) { + throw VMError.intentionallyUnimplemented(); + } + + @Override + public void clearStepping(Thread thread) { + throw VMError.intentionallyUnimplemented(); + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/EventHandler.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/EventHandler.java new file mode 100644 index 000000000000..c5271301be91 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/EventHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +import jdk.vm.ci.meta.ResolvedJavaMethod; + +public interface EventHandler { + /** + * An event at a program location. The bit flags may represent following event kinds: + * {@link EventKind#BREAKPOINT}, {@link EventKind#SINGLE_STEP}, {@link EventKind#METHOD_ENTRY}, + * {@link EventKind#METHOD_EXIT}, {@link EventKind#METHOD_EXIT_WITH_RETURN_VALUE}. + */ + void onEventAt(Thread thread, ResolvedJavaMethod method, int bci, Object result, int eventKindFlags); +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/EventKind.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/EventKind.java new file mode 100644 index 000000000000..7a8fcc579c17 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/EventKind.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +/** + * The EventKind from the JDWP protocol. There is a bridge copy + * com.oracle.svm.jdwp.bridge.EventKind, the enums are exchanged via ordinal number + * ({@link EventKind#ordinal()} and {@link EventKind#fromOrdinal(int)}). + */ +public enum EventKind { + + SINGLE_STEP(1), + BREAKPOINT(2), + FRAME_POP(3), + EXCEPTION(4), + USER_DEFINED(5), + THREAD_START(6), + THREAD_DEATH(7), + CLASS_PREPARE(8), + CLASS_UNLOAD(9), + CLASS_LOAD(10), + FIELD_ACCESS(20), + FIELD_MODIFICATION(21), + EXCEPTION_CATCH(30), + METHOD_ENTRY(40), + METHOD_EXIT(41), + METHOD_EXIT_WITH_RETURN_VALUE(42), + MONITOR_CONTENDED_ENTER(43), + MONITOR_CONTENDED_ENTERED(44), + MONITOR_WAIT(45), + MONITOR_WAITED(46), + VM_START(90), + VM_DEATH(99), + VM_DISCONNECTED(100); + + EventKind(int id) { + assert 0 < id && id < 127 : id; + } + + private static final EventKind[] VALUES = EventKind.values(); + + public static EventKind fromOrdinal(int ordinal) { + return VALUES[ordinal]; + } + + /** + * Flag of the event used by + * {@link EventHandler#onEventAt(Thread, jdk.vm.ci.meta.ResolvedJavaMethod, int, Object, int)}. + * The only events on the same thread and at the same location that can be combined to bit flags + * are: {@link #BREAKPOINT}, {@link #SINGLE_STEP}, {@link #METHOD_ENTRY}, {@link #METHOD_EXIT}, + * {@link #METHOD_EXIT_WITH_RETURN_VALUE} + */ + public int getFlag() { + assert ordinal() < 32 : "Flag overflow, ordinal = " + ordinal(); + int flag = 1 << ordinal(); + return flag; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/Location.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/Location.java new file mode 100644 index 000000000000..a89986a9c8e3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/Location.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +import jdk.vm.ci.meta.JavaMethod; +import jdk.vm.ci.meta.LineNumberTable; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +import static com.oracle.svm.core.code.FrameSourceInfo.LINENUMBER_NATIVE; +import static com.oracle.svm.core.code.FrameSourceInfo.LINENUMBER_UNKNOWN; + +public interface Location { + int LINENUMBER_MARKER = LINENUMBER_NATIVE; + + JavaMethod method(); + + int bci(); + + int lineNumber(); + + static Location create(ResolvedJavaMethod method, int bci) { + return create(method, bci, LINENUMBER_MARKER); + } + + static Location create(ResolvedJavaMethod method, int bci, int lineNo) { + return new Location() { + private int lineNumber = lineNo; + + @Override + public JavaMethod method() { + return method; + } + + @Override + public int bci() { + return bci; + } + + @Override + public int lineNumber() { + if (lineNumber != LINENUMBER_MARKER) { + return lineNumber; + } + LineNumberTable lineNumberTable = method.getLineNumberTable(); + if (lineNumberTable != null) { + lineNumber = lineNumberTable.getLineNumber(bci); + } else { + lineNumber = LINENUMBER_UNKNOWN; + } + return lineNumber; + } + }; + } + + /** + * Returns the line number associated with a BCI, or -1 (LINENUMBER_UNKNOWN) if the method is + * native or no line information is available. + */ + static int getLineNumber(ResolvedJavaMethod method, int bci) { + if (method.isNative()) { + return LINENUMBER_UNKNOWN; + } + LineNumberTable lineNumberTable = method.getLineNumberTable(); + if (lineNumberTable != null) { + return lineNumberTable.getLineNumber(bci); + } + return LINENUMBER_UNKNOWN; + } +} diff --git a/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/SteppingControl.java b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/SteppingControl.java new file mode 100644 index 000000000000..92e54c6360a3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.interpreter/src/com/oracle/svm/interpreter/debug/SteppingControl.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.interpreter.debug; + +import com.oracle.svm.core.util.VMError; + +import jdk.vm.ci.meta.ResolvedJavaMethod; + +public final class SteppingControl { + + /** + * Step into any newly pushed frames. + */ + public static final int STEP_INTO = 1; + /** + * Step over any newly pushed frames. + */ + public static final int STEP_OVER = 2; + /** + * Step out of the current frame. + */ + public static final int STEP_OUT = 3; + + /** + * Step to the next available location. + */ + public static final int STEP_MIN = -1; + /** + * Step to the next location on a different line. + */ + public static final int STEP_LINE = -2; + + private final Thread thread; + private final int depth; + private final int size; + + private int currentFrameDepth; + + // Location where the stepping request was set, required only to check line numbers when + // STEP_LINE is specified. + private Location startingLocation; + + public boolean isActiveAtCurrentFrameDepth() { + switch (getDepth()) { + case STEP_OUT: + return currentFrameDepth < 0; // fire events only on outer frames + case STEP_OVER: + return currentFrameDepth <= 0; // fire events in the starting and outer frames + case STEP_INTO: + return true; // fire events in all frames + default: + throw VMError.shouldNotReachHere("unknown stepping depth"); + } + } + + public SteppingControl(Thread thread, int depth, int size) { + VMError.guarantee(size == STEP_MIN || size == STEP_LINE); + VMError.guarantee(depth == STEP_INTO || depth == STEP_OVER || depth == STEP_OUT); + this.thread = thread; + this.depth = depth; + this.size = size; + this.currentFrameDepth = 0; + } + + public int getSize() { + return size; + } + + public int getDepth() { + return depth; + } + + public Thread getThread() { + return thread; + } + + public void setStartingLocation(Location location) { + this.startingLocation = location; + } + + public boolean withinSameLine(ResolvedJavaMethod method, int bci) { + if (startingLocation == null) { + // No starting location set, consider it new line hit. + return false; + } + if (!method.equals(startingLocation.method())) { + return false; + } + // Might need to handle case when no line information is available + int startingLine = startingLocation.lineNumber(); + int currentLine = Location.getLineNumber(method, bci); + return (currentLine == startingLine); + } + + public void popFrame() { + --currentFrameDepth; + } + + public void pushFrame() { + ++currentFrameDepth; + } + +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ArgFilesOption.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ArgFilesOption.java new file mode 100644 index 000000000000..22f7062fc760 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ArgFilesOption.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Utility to parse @argFile arguments. Copied and slightly adapted from + * {@code com.oracle.svm.driver.ArgFilesOptionPreprocessor}. + * + * GR-55150: Merge back into com.oracle.svm.driver.ArgFilesOptionPreprocessor. + * + * @see @argument + * files + */ +public final class ArgFilesOption { + + private static final String DISABLE_AT_FILES_OPTION = "--disable-@files"; + + private boolean disableAtFiles = false; + + public List process(String currentArg) { + switch (currentArg) { + case DISABLE_AT_FILES_OPTION: + disableAtFiles = true; + return List.of(); + } + + if (!disableAtFiles && currentArg.startsWith("@")) { + Path argFile = Paths.get(currentArg.substring(1)); + return readArgFile(argFile); + } + + return List.of(currentArg); + } + + // Ported from JDK11's java.base/share/native/libjli/args.c + private enum PARSER_STATE { + FIND_NEXT, + IN_COMMENT, + IN_QUOTE, + IN_ESCAPE, + SKIP_LEAD_WS, + IN_TOKEN + } + + private static class CTX_ARGS { + PARSER_STATE state; + int cptr; + int eob; + char quoteChar; + List parts; + String options; + } + + // Ported from JDK11's java.base/share/native/libjli/args.c + public List readArgFile(Path file) { + List arguments = new ArrayList<>(); + + String options = null; + try { + options = new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot read argument file '" + file + "'"); + } + + CTX_ARGS ctx = new CTX_ARGS(); + ctx.state = PARSER_STATE.FIND_NEXT; + ctx.parts = new ArrayList<>(4); + ctx.quoteChar = '"'; + ctx.cptr = 0; + ctx.eob = options.length(); + ctx.options = options; + + String token = nextToken(ctx); + while (token != null) { + addArg(arguments, token); + token = nextToken(ctx); + } + + // remaining partial token + if (ctx.state == PARSER_STATE.IN_TOKEN || ctx.state == PARSER_STATE.IN_QUOTE) { + if (ctx.parts.size() != 0) { + token = String.join("", ctx.parts); + addArg(arguments, token); + } + } + return arguments; + } + + private void addArg(List args, String arg) { + Objects.requireNonNull(arg); + if (DISABLE_AT_FILES_OPTION.equals(arg)) { + disableAtFiles = true; + } else { + args.add(arg); + } + } + + // Ported from JDK11's java.base/share/native/libjli/args.c + @SuppressWarnings("fallthrough") + private static String nextToken(CTX_ARGS ctx) { + int nextc = ctx.cptr; + int eob = ctx.eob; + int anchor = nextc; + String token; + + for (; nextc < eob; nextc++) { + char ch = ctx.options.charAt(nextc); + + // Skip white space characters + if (ctx.state == PARSER_STATE.FIND_NEXT || ctx.state == PARSER_STATE.SKIP_LEAD_WS) { + while (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' || ch == '\f') { + nextc++; + if (nextc >= eob) { + return null; + } + ch = ctx.options.charAt(nextc); + } + ctx.state = (ctx.state == PARSER_STATE.FIND_NEXT) ? PARSER_STATE.IN_TOKEN : PARSER_STATE.IN_QUOTE; + anchor = nextc; + // Deal with escape sequences + } else if (ctx.state == PARSER_STATE.IN_ESCAPE) { + // concatenation directive + if (ch == '\n' || ch == '\r') { + ctx.state = PARSER_STATE.SKIP_LEAD_WS; + } else { + // escaped character + String escaped; + switch (ch) { + case 'n': + escaped = "\n"; + break; + case 'r': + escaped = "\r"; + break; + case 't': + escaped = "\t"; + break; + case 'f': + escaped = "\f"; + break; + default: + escaped = String.valueOf(ch); + break; + } + ctx.parts.add(escaped); + ctx.state = PARSER_STATE.IN_QUOTE; + } + // anchor to next character + anchor = nextc + 1; + continue; + // ignore comment to EOL + } else if (ctx.state == PARSER_STATE.IN_COMMENT) { + while (ch != '\n' && ch != '\r') { + nextc++; + if (nextc >= eob) { + return null; + } + ch = ctx.options.charAt(nextc); + } + anchor = nextc + 1; + ctx.state = PARSER_STATE.FIND_NEXT; + continue; + } + + assert (ctx.state != PARSER_STATE.IN_ESCAPE); + assert (ctx.state != PARSER_STATE.FIND_NEXT); + assert (ctx.state != PARSER_STATE.SKIP_LEAD_WS); + assert (ctx.state != PARSER_STATE.IN_COMMENT); + + switch (ch) { + case ' ': + case '\t': + case '\f': + if (ctx.state == PARSER_STATE.IN_QUOTE) { + continue; + } + // fall through + case '\n': + case '\r': + if (ctx.parts.size() == 0) { + token = ctx.options.substring(anchor, nextc); + } else { + ctx.parts.add(ctx.options.substring(anchor, nextc)); + token = String.join("", ctx.parts); + ctx.parts = new ArrayList<>(); + } + ctx.cptr = nextc + 1; + ctx.state = PARSER_STATE.FIND_NEXT; + return token; + case '#': + if (ctx.state == PARSER_STATE.IN_QUOTE) { + continue; + } + ctx.state = PARSER_STATE.IN_COMMENT; + anchor = nextc + 1; + break; + case '\\': + if (ctx.state != PARSER_STATE.IN_QUOTE) { + continue; + } + ctx.parts.add(ctx.options.substring(anchor, nextc)); + ctx.state = PARSER_STATE.IN_ESCAPE; + // anchor after backslash character + anchor = nextc + 1; + break; + case '\'': + case '"': + if (ctx.state == PARSER_STATE.IN_QUOTE && ctx.quoteChar != ch) { + // not matching quote + continue; + } + // partial before quote + if (anchor != nextc) { + ctx.parts.add(ctx.options.substring(anchor, nextc)); + } + // anchor after quote character + anchor = nextc + 1; + if (ctx.state == PARSER_STATE.IN_TOKEN) { + ctx.quoteChar = ch; + ctx.state = PARSER_STATE.IN_QUOTE; + } else { + ctx.state = PARSER_STATE.IN_TOKEN; + } + break; + default: + break; + } + } + + assert (nextc == eob); + // Only need partial token, not comment or whitespaces + if (ctx.state == PARSER_STATE.IN_TOKEN || ctx.state == PARSER_STATE.IN_QUOTE) { + if (anchor < nextc) { + // not yet return until end of stream, we have part of a token. + ctx.parts.add(ctx.options.substring(anchor, nextc)); + } + } + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/CheckedReader.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/CheckedReader.java new file mode 100644 index 000000000000..27ca9f338207 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/CheckedReader.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +public final class CheckedReader { + + private final SymbolicRefs symbolicRefs; + + public CheckedReader(SymbolicRefs symbolicRefs) { + this.symbolicRefs = symbolicRefs; + } + + public ResolvedJavaType readTypeRef(Packet.Reader input) throws JDWPException { + return readTypeRef(input, false); + } + + public long readTypeRefId(Packet.Reader input, boolean allowNull) throws JDWPException { + long typeRefId = input.readLong(); + if (typeRefId == 0L) { + if (!allowNull) { + throw JDWPException.raise(ErrorCode.INVALID_CLASS); + } + return 0L; + } + symbolicRefs.toResolvedJavaType(typeRefId); // may throw JDWPException + return typeRefId; + } + + public long readMethodRefId(Packet.Reader input, boolean allowNull) throws JDWPException { + long methodRefId = input.readLong(); + if (methodRefId == 0L) { + if (!allowNull) { + throw JDWPException.raise(ErrorCode.INVALID_METHODID); + } + return 0L; + } + symbolicRefs.toResolvedJavaMethod(methodRefId); // may throw JDWPException + return methodRefId; + } + + public long readFieldRefId(Packet.Reader input, boolean allowNull) throws JDWPException { + long fieldRefId = input.readLong(); + if (fieldRefId == 0L) { + if (!allowNull) { + throw JDWPException.raise(ErrorCode.INVALID_FIELDID); + } + return 0L; + } + symbolicRefs.toResolvedJavaField(fieldRefId); // may throw JDWPException + return fieldRefId; + } + + public ResolvedJavaType readTypeRef(Packet.Reader input, boolean allowNull) throws JDWPException { + long typeRefId = readTypeRefId(input, allowNull); + if (typeRefId == 0L && allowNull) { + return null; + } + return symbolicRefs.toResolvedJavaType(typeRefId); + } + + public ResolvedJavaMethod readMethodRef(Packet.Reader input) throws JDWPException { + return readMethodRef(input, false); + } + + public ResolvedJavaMethod readMethodRef(Packet.Reader input, boolean allowNull) throws JDWPException { + long methodRefId = readMethodRefId(input, allowNull); + if (methodRefId == 0L) { + return null; + } + return symbolicRefs.toResolvedJavaMethod(methodRefId); + } + + public ResolvedJavaField readFieldRef(Packet.Reader input, boolean allowNull) throws JDWPException { + long fieldRefId = readTypeRefId(input, allowNull); + if (fieldRefId == 0L) { + return null; + } + return symbolicRefs.toResolvedJavaField(fieldRefId); + } + + public ResolvedJavaField readFieldRef(Packet.Reader input) throws JDWPException { + return readFieldRef(input, false); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ClassStatusConstants.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ClassStatusConstants.java new file mode 100644 index 000000000000..1f56e59188a9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ClassStatusConstants.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +/** + * Constants to mimic the status of classes in a VM. + */ +public final class ClassStatusConstants { + public static final int LOADED = 0; + public static final int VERIFIED = 1; + public static final int PREPARED = 2; + public static final int INITIALIZED = 4; + public static final int ERROR = 8; + + private ClassStatusConstants() { + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/DebugOptions.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/DebugOptions.java new file mode 100644 index 000000000000..7c37c2e7438e --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/DebugOptions.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public final class DebugOptions { + + /** + * Supported JDWP options. + */ + public record Options(String jdwpOptions, + String additionalOptions, + boolean help, + boolean server, + boolean suspend, + boolean quiet, + int timeout, + String address, + String host, + int port, + String transport, + String mode, + String libraryPath, + String vmOptions, + boolean tracing, + Map unknownOptions) { + + public boolean hasUnknownOptions() { + return unknownOptions != null && !unknownOptions.isEmpty(); + } + } + + private static Map parseKeyValues(String text) { + Map options = new HashMap<>(); + for (int index = 0; index < text.length();) { + int equalsIndex = text.indexOf('=', index); + if (equalsIndex < 0) { + throw new IllegalArgumentException("Invalid key=value at index " + index); + } + String key = text.substring(index, equalsIndex); + int nextIndex = text.indexOf(',', equalsIndex + 1); + if (nextIndex < 0) { + nextIndex = text.length(); + } + String value = text.substring(equalsIndex + 1, nextIndex); + if (options.containsKey(key)) { + throw new IllegalArgumentException("Repeated key " + key); + } + options.put(key, value); + index = nextIndex + 1; + } + return options; + } + + private static String checkAllowedValues(String key, String value, String... allowedValues) { + for (String allowedValue : allowedValues) { + if (Objects.equals(value, allowedValue)) { + return value; + } + } + throw new IllegalArgumentException("Invalid entry " + key + "=" + value + " allowed values: " + String.join(", ", allowedValues)); + } + + private static int checkInt(String key, String value, int lowerBound, int upperBound) { + int intValue; + try { + intValue = Integer.parseInt(value); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Invalid entry " + key + "=" + value + " not an integer"); + } + if (!(lowerBound <= intValue && intValue <= upperBound)) { + throw new IllegalArgumentException("Invalid entry " + key + "=" + value + " must be within [" + lowerBound + ", " + upperBound + "]"); + } + return intValue; + } + + public static Options parse(String jdwpOptions, boolean throwIfUnknown) { + return parse(jdwpOptions, null, throwIfUnknown, false); + } + + /** + * Parses JDWP options. This method only validates common known options e.g. doesn't check if + * required options are present. + * + * @param throwIfUnknown if true, throws {@link IllegalArgumentException} if there's an + * unknown/unsupported options + * @throws IllegalArgumentException if options cannot be parsed (malformed) + */ + public static Options parse(String jdwpOptions, String additionalOptions, boolean throwIfUnknown, boolean tracing) { + + boolean help = false; + boolean server = false; // n + boolean suspend = true; // y + boolean quiet = false; // n + int timeout = 0; // no timeout + String address = null; + String host = null; + int port = 0; + String transport = null; + String mode = "native"; + String libraryPath = null; + String vmOptions = null; + + Map unknownOptions = new HashMap<>(); + + help = "help".equals(jdwpOptions); + + String combinedOptions = help ? "" : jdwpOptions; + if (additionalOptions != null && !additionalOptions.isEmpty()) { + if (!combinedOptions.isEmpty()) { + combinedOptions += ","; + } + combinedOptions += additionalOptions; + } + Map keyValues = parseKeyValues(combinedOptions); + + for (Map.Entry entry : keyValues.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + switch (key) { + case "server" -> server = checkAllowedValues(key, value, "y", "n").equals("y"); + case "suspend" -> suspend = checkAllowedValues(key, value, "y", "n").equals("y"); + case "quiet" -> quiet = checkAllowedValues(key, value, "y", "n").equals("y"); + case "timeout" -> timeout = checkInt(key, value, 0, Integer.MAX_VALUE); + // GR-55160: Revisit adding support for dt_shmem (on Windows). + case "transport" -> transport = checkAllowedValues(key, value, "dt_socket"); + case "address" -> { + String[] parts = value.split(":"); + String inputHost = null; + String inputPort; + if (parts.length == 1) { + inputPort = parts[0]; + } else if (parts.length == 2) { + inputHost = parts[0]; + inputPort = parts[1]; + } else { + throw new IllegalArgumentException("Invalid entry " + key + "=" + value + " not a '[host:]port' pair"); + } + address = value; + host = inputHost; + port = checkInt(key, inputPort, 0, 65535); + } + case "mode" -> { + String[] parts = value.split(":", 2); + mode = checkAllowedValues(key, parts[0], "native", "jvm"); + if (parts.length > 1) { + // native:/path/to/libsvmdebugger.so + // jvm:/path/to/libjvm.so + libraryPath = parts[1]; + } else { + libraryPath = null; + } + } + case "vm.options" -> { + // VM options passed to the JVM or native isolate where the JDWP server + // runs. + vmOptions = value; + } + default -> { + if (throwIfUnknown) { + throw new IllegalArgumentException("Unknown/unsupported option: " + key); + } else { + unknownOptions.put(key, value); + } + } + } + } + + return new Options(jdwpOptions, additionalOptions, help, server, suspend, quiet, timeout, address, host, port, transport, mode, libraryPath, vmOptions, tracing, unknownOptions); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ErrorCode.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ErrorCode.java new file mode 100644 index 000000000000..273c85c74301 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ErrorCode.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +//@formatter:off +public enum ErrorCode { + NONE(0, "No error has occurred."), + INVALID_THREAD(10, "The thread is null or not a valid thread."), + INVALID_THREAD_GROUP(11, "Thread group invalid."), + INVALID_PRIORITY(12, "Invalid priority."), + THREAD_NOT_SUSPENDED(13, "If the specified thread has not been suspended by an event."), + THREAD_SUSPENDED(14, "Thread already suspended."), + THREAD_NOT_ALIVE(15, "Not used."), + INVALID_OBJECT(20, "If this reference type has been unloaded and garbage collected."), + INVALID_CLASS(21, "Invalid class."), + CLASS_NOT_PREPARED(22, "Class has been loaded but not yet prepared."), + INVALID_METHODID(23, "Invalid method."), + INVALID_LOCATION(24, "Invalid location."), + INVALID_FIELDID(25, "Invalid field."), + INVALID_FRAMEID(30, "Invalid jframeID."), + NO_MORE_FRAMES(31, "There are no more Java or JNI frames on the call stack."), + OPAQUE_FRAME(32, "Information about the frame is not available (e.g. native frame) or the target VM is unable to perform an operation on the thread's current frame."), + NOT_CURRENT_FRAME(33, "Operation can only be performed on current frame."), + TYPE_MISMATCH(34, "The variable is not an appropriate type for the function used."), + INVALID_SLOT(35, "Invalid slot."), + DUPLICATE(40, "Item already set."), + NOT_FOUND(41, "Desired element not found."), + INVALID_MODULE(42, "Invalid module."), + INVALID_MONITOR(50, "Invalid monitor."), + NOT_MONITOR_OWNER(51, "This thread doesn't own the monitor."), + INTERRUPT(52, "The call has been interrupted before completion."), + INVALID_CLASS_FORMAT(60, "The virtual machine attempted to read a class file and determined that the file is malformed or otherwise cannot be interpreted as a class file."), + CIRCULAR_CLASS_DEFINITION(61, "A circularity has been detected while initializing a class."), + FAILS_VERIFICATION(62, "The verifier detected that a class file, though well formed, contained some sort of internal inconsistency or security problem."), + ADD_METHOD_NOT_IMPLEMENTED(63, "Adding methods has not been implemented."), + SCHEMA_CHANGE_NOT_IMPLEMENTED(64, "Schema change has not been implemented."), + INVALID_TYPESTATE(65, "The state of the thread has been modified, and is now inconsistent."), + HIERARCHY_CHANGE_NOT_IMPLEMENTED(66, "A direct superclass is different for the new class version, or the set of directly implemented interfaces is different and canUnrestrictedlyRedefineClasses is false."), + DELETE_METHOD_NOT_IMPLEMENTED(67, "The new class version does not declare a method declared in the old class version and canUnrestrictedlyRedefineClasses is false."), + UNSUPPORTED_VERSION(68, "A class file has a version number not supported by this VM."), + NAMES_DONT_MATCH(69, "The class name defined in the new class file is different from the name in the old class object."), + CLASS_MODIFIERS_CHANGE_NOT_IMPLEMENTED(70, "The new class version has different modifiers and canUnrestrictedlyRedefineClasses is false."), + METHOD_MODIFIERS_CHANGE_NOT_IMPLEMENTED(71, "A method in the new class version has different modifiers than its counterpart in the old class version and canUnrestrictedlyRedefineClasses is false."), + CLASS_ATTRIBUTE_CHANGE_NOT_IMPLEMENTED(72, "The new class version has a different NestHost, NestMembers, PermittedSubclasses, or Record class attribute and canUnrestrictedlyRedefineClasses is false."), + NOT_IMPLEMENTED(99, "The functionality is not implemented in this virtual machine."), + NULL_POINTER(100, "Invalid pointer."), + ABSENT_INFORMATION(101, "Desired information is not available."), + INVALID_EVENT_TYPE(102, "The specified event type id is not recognized."), + ILLEGAL_ARGUMENT(103, "Illegal argument."), + OUT_OF_MEMORY(110, "The function needed to allocate memory and no more memory was available for allocation."), + ACCESS_DENIED(111, "Debugging has not been enabled in this virtual machine. JVMTI cannot be used."), + VM_DEAD(112, "The virtual machine is not running."), + INTERNAL(113, "An unexpected internal error has occurred."), + UNATTACHED_THREAD(115, "The thread being used to call this function is not attached to the virtual machine. Calls must be made from attached threads."), + INVALID_TAG(500, "object type id or class tag."), + ALREADY_INVOKING(502, "Previous invoke not complete."), + INVALID_INDEX(503, "Index is invalid."), + INVALID_LENGTH(504, "The length is invalid."), + INVALID_STRING(506, "The string is invalid."), + INVALID_CLASS_LOADER(507, "The class loader is invalid."), + INVALID_ARRAY(508, "The array is invalid."), + TRANSPORT_LOAD(509, "Unable to load the transport."), + TRANSPORT_INIT(510, "Unable to initialize the transport."), + NATIVE_METHOD(511, ""), + INVALID_COUNT(512, "The count is invalid."); + + private final int errorCode; + private final String message; + + ErrorCode(int errorCode, String message) { + this.errorCode = errorCode; + this.message = message; + } + + public int value() { + return errorCode; + } + + public String getMessage() { + return message; + } +} +//@formatter:on diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/EventHandlerBridge.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/EventHandlerBridge.java new file mode 100644 index 000000000000..13428b540cb7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/EventHandlerBridge.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public interface EventHandlerBridge { + void onEventAt(long threadId, long classId, byte typeTag, long methodId, int bci, byte resultTag, long resultPrimitiveOrId, int eventKindFlags); + + void onVMDeath(); + + void onThreadStart(long threadId); + + void onThreadDeath(long threadId); +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/EventKind.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/EventKind.java new file mode 100644 index 000000000000..7ad723c22059 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/EventKind.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +/** + * The EventKind from the JDWP protocol. There is a interpreter copy + * com.oracle.svm.interpreter.debug.EventKind, the enums are exchanged via ordinal + * number ({@link EventKind#ordinal()} and {@link EventKind#fromOrdinal(int)}). + */ +public enum EventKind { + + SINGLE_STEP(1), + BREAKPOINT(2), + FRAME_POP(3), + EXCEPTION(4), + USER_DEFINED(5), + THREAD_START(6), + THREAD_DEATH(7), + CLASS_PREPARE(8), + CLASS_UNLOAD(9), + CLASS_LOAD(10), + FIELD_ACCESS(20), + FIELD_MODIFICATION(21), + EXCEPTION_CATCH(30), + METHOD_ENTRY(40), + METHOD_EXIT(41), + METHOD_EXIT_WITH_RETURN_VALUE(42), + MONITOR_CONTENDED_ENTER(43), + MONITOR_CONTENDED_ENTERED(44), + MONITOR_WAIT(45), + MONITOR_WAITED(46), + VM_START(90), + VM_DEATH(99), + VM_DISCONNECTED(100); + + private final byte eventId; + + EventKind(int id) { + assert 0 < id && id < 127 : id; + this.eventId = (byte) id; + } + + private static final EventKind[] VALUES = EventKind.values(); + + public byte getEventId() { + return eventId; + } + + public static EventKind fromOrdinal(int ordinal) { + return VALUES[ordinal]; + } + + public static EventKind of(byte eventId) { + // find the EventKind using binary search: + int low = 0; + int high = VALUES.length - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + EventKind midKind = VALUES[mid]; + if (midKind.eventId < eventId) { + low = mid + 1; + } else if (midKind.eventId > eventId) { + high = mid - 1; + } else { + return midKind; + } + } + return null; + } + + /** + * Test if the bit flags contain this event kind. + */ + public boolean matchesFlag(int flags) { + assert ordinal() < 32 : "Flag overflow, ordinal = " + ordinal(); + int myFlag = 1 << ordinal(); + return (flags & myFlag) != 0; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/FrameId.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/FrameId.java new file mode 100644 index 000000000000..6a098419ab08 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/FrameId.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public final class FrameId { + private static final int FRAME_DEPTH_BITS = 16; + private static final int FRAME_DEPTH_MASK = (1 << FRAME_DEPTH_BITS) - 1; + + public static int getFrameDepth(long frameId) { + return (int) (frameId & FRAME_DEPTH_MASK); + } + + public static long getFrameGeneration(long frameId) { + return frameId >>> FRAME_DEPTH_BITS; + } + + public static long createFrameId(long frameGeneration, int frameDepth) { + assert frameDepth < (1 << FRAME_DEPTH_BITS); + assert frameGeneration < (1L << (Long.SIZE - FRAME_DEPTH_BITS)); + return (frameGeneration << FRAME_DEPTH_BITS) | frameDepth; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/HSToNativeJDWPBridge.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/HSToNativeJDWPBridge.java new file mode 100644 index 000000000000..555cc687c0b9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/HSToNativeJDWPBridge.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.jdwp.bridge.nativebridge.NativeIsolate; +import com.oracle.svm.jdwp.bridge.nativebridge.NativeObject; + +// Originally generated with nativebridge +// @GenerateHotSpotToNativeBridge(jniConfig = JDWPJNIConfig.class, include = ResidentJDWPFeatureEnabled.class) +public abstract class HSToNativeJDWPBridge extends NativeObject implements JDWPBridge { + public HSToNativeJDWPBridge(NativeIsolate isolate, long objectHandle) { + super(isolate, objectHandle); + } + + public static HSToNativeJDWPBridge createHSToNative(NativeIsolate isolate, long objectHandle) { + return HSToNativeJDWPBridgeGen.createHSToNative(isolate, objectHandle); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/HSToNativeJDWPBridgeGen.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/HSToNativeJDWPBridgeGen.java new file mode 100644 index 000000000000..ccc2262c8271 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/HSToNativeJDWPBridgeGen.java @@ -0,0 +1,868 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +//Checkstyle: stop +// @formatter:off +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JByteArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JClass; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JIntArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JLongArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JString; +import com.oracle.svm.jdwp.bridge.jniutils.JNIMethodScope; +import com.oracle.svm.jdwp.bridge.jniutils.JNIUtil; +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryInput; +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryMarshaller; +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput; +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.CCharPointerBinaryOutput; +import com.oracle.svm.jdwp.bridge.nativebridge.ForeignException; +import com.oracle.svm.jdwp.bridge.nativebridge.JNIConfig; +import com.oracle.svm.jdwp.bridge.nativebridge.NativeIsolate; +import com.oracle.svm.jdwp.bridge.nativebridge.NativeIsolateThread; +import com.oracle.svm.jdwp.bridge.nativebridge.NativeObjectHandles; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.UnmanagedMemory; +import org.graalvm.nativeimage.c.function.CEntryPoint; +import org.graalvm.nativeimage.c.function.CEntryPoint.IsolateThreadContext; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.word.WordFactory; + +/* Checkout README.md before modifying */ +final class HSToNativeJDWPBridgeGen { + + static HSToNativeJDWPBridge createHSToNative(NativeIsolate isolate, long objectHandle) { + return new HSToNativeStartPoint(isolate, objectHandle); + } + + private static final class HSToNativeStartPoint extends HSToNativeJDWPBridge { + + private static final BinaryMarshaller packetMarshaller; + private static final BinaryMarshaller stackFrameMarshaller; + private static final BinaryMarshaller throwableMarshaller; + + static { + JNIConfig config = JDWPJNIConfig.getInstance(); + packetMarshaller = config.lookupMarshaller(Packet.class); + stackFrameMarshaller = config.lookupMarshaller(StackFrame.class); + throwableMarshaller = config.lookupMarshaller(Throwable.class); + } + + HSToNativeStartPoint(NativeIsolate isolate, long objectHandle) { + super(isolate, objectHandle); + } + + @Override + public void clearStepping(long threadId) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + clearStepping0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), threadId); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public String currentWorkingDirectory() { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return currentWorkingDirectory0(nativeIsolateThread.getIsolateThreadId(), this.getHandle()); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public Packet dispatch(Packet packet) throws JDWPException { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + int marshalledParametersSizeEstimate = packetMarshaller.inferSize(packet); + BinaryOutput.ByteArrayBinaryOutput marshalledParametersOutput = BinaryOutput.ByteArrayBinaryOutput.create(marshalledParametersSizeEstimate); + packetMarshaller.write(marshalledParametersOutput, packet); + byte[] endPointResult = dispatch0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), marshalledParametersOutput.getArray()); + BinaryInput marshalledResultInput = BinaryInput.create(endPointResult); + return packetMarshaller.read(marshalledResultInput); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public int fieldRefIdToIndex(long fieldRefId) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return fieldRefIdToIndex0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), fieldRefId); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public long fieldRefIndexToId(int fieldRefIndex) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return fieldRefIndexToId0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), fieldRefIndex); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public long getCurrentThis() { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return getCurrentThis0(nativeIsolateThread.getIsolateThreadId(), this.getHandle()); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public String getSystemProperty(String key) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return getSystemProperty0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), key); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public StackFrame[] getThreadFrames(long threadId) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + byte[] endPointResult = getThreadFrames0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), threadId); + BinaryInput marshalledResultInput = BinaryInput.create(endPointResult); + int hsResultLength = marshalledResultInput.readInt(); + StackFrame[] hsResult; + if (hsResultLength != -1) { + hsResult = new StackFrame[hsResultLength]; + for(int hsResultIndex = 0; hsResultIndex < hsResultLength; hsResultIndex++) { + StackFrame hsResultElement = stackFrameMarshaller.read(marshalledResultInput); + hsResult[hsResultIndex] = hsResultElement; + } + } else { + hsResult = null; + } + return hsResult; + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public int getThreadStatus(long threadId) throws JDWPException { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return getThreadStatus0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), threadId); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public boolean isCurrentThreadVirtual() { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return isCurrentThreadVirtual0(nativeIsolateThread.getIsolateThreadId(), this.getHandle()); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public boolean isEventEnabled(long threadId, int eventKind) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return isEventEnabled0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), threadId, eventKind); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public int methodRefIdToIndex(long methodRefId) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return methodRefIdToIndex0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), methodRefId); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public long methodRefIndexToId(int methodRefIndex) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return methodRefIndexToId0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), methodRefIndex); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public void setEventEnabled(long threadId, int eventKind, boolean enable) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + setEventEnabled0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), threadId, eventKind, enable); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public void setStepping(long threadId, int depth, int size) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + setStepping0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), threadId, depth, size); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public void setSteppingFromLocation(long threadId, int depth, int size, long methodId, int bci, int lineNumber) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + setSteppingFromLocation0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), threadId, depth, size, methodId, bci, lineNumber); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public void setThreadRequest(boolean start, boolean enable) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + setThreadRequest0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), start, enable); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public long threadResume(long suspendId) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return threadResume0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), suspendId); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public long threadSuspend(long threadId) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return threadSuspend0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), threadId); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public void toggleBreakpoint(long methodId, int bci, boolean enable) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + toggleBreakpoint0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), methodId, bci, enable); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public void toggleMethodEnterEvent(long clazzId, boolean enable) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + toggleMethodEnterEvent0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), clazzId, enable); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public void toggleMethodExitEvent(long clazzId, boolean enable) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + toggleMethodExitEvent0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), clazzId, enable); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public int typeRefIdToIndex(long typeRefId) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return typeRefIdToIndex0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), typeRefId); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public long typeRefIndexToId(int typeRefIndex) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return typeRefIndexToId0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), typeRefIndex); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public int[] typeStatus(long... typeIds) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return typeStatus0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), typeIds); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public void vmResume(long[] ignoredThreadIds) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + vmResume0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), ignoredThreadIds); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + @Override + public long[] vmSuspend(long[] ignoredThreadIds) { + NativeIsolateThread nativeIsolateThread = this.getIsolate().enter(); + try { + return vmSuspend0(nativeIsolateThread.getIsolateThreadId(), this.getHandle(), ignoredThreadIds); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } finally { + nativeIsolateThread.leave(); + } + } + + private static native void clearStepping0(long isolateThread, long objectId, long threadId); + + private static native String currentWorkingDirectory0(long isolateThread, long objectId); + + private static native byte[] dispatch0(long isolateThread, long objectId, byte[] marshalledData) throws JDWPException; + + private static native int fieldRefIdToIndex0(long isolateThread, long objectId, long fieldRefId); + + private static native long fieldRefIndexToId0(long isolateThread, long objectId, int fieldRefIndex); + + private static native long getCurrentThis0(long isolateThread, long objectId); + + private static native String getSystemProperty0(long isolateThread, long objectId, String key); + + private static native byte[] getThreadFrames0(long isolateThread, long objectId, long threadId); + + private static native int getThreadStatus0(long isolateThread, long objectId, long threadId) throws JDWPException; + + private static native boolean isCurrentThreadVirtual0(long isolateThread, long objectId); + + private static native boolean isEventEnabled0(long isolateThread, long objectId, long threadId, int eventKind); + + private static native int methodRefIdToIndex0(long isolateThread, long objectId, long methodRefId); + + private static native long methodRefIndexToId0(long isolateThread, long objectId, int methodRefIndex); + + private static native void setEventEnabled0(long isolateThread, long objectId, long threadId, int eventKind, boolean enable); + + private static native void setStepping0(long isolateThread, long objectId, long threadId, int depth, int size); + + private static native void setSteppingFromLocation0(long isolateThread, long objectId, long threadId, int depth, int size, long methodId, int bci, int lineNumber); + + private static native void setThreadRequest0(long isolateThread, long objectId, boolean start, boolean enable); + + private static native long threadResume0(long isolateThread, long objectId, long suspendId); + + private static native long threadSuspend0(long isolateThread, long objectId, long threadId); + + private static native void toggleBreakpoint0(long isolateThread, long objectId, long methodId, int bci, boolean enable); + + private static native void toggleMethodEnterEvent0(long isolateThread, long objectId, long clazzId, boolean enable); + + private static native void toggleMethodExitEvent0(long isolateThread, long objectId, long clazzId, boolean enable); + + private static native int typeRefIdToIndex0(long isolateThread, long objectId, long typeRefId); + + private static native long typeRefIndexToId0(long isolateThread, long objectId, int typeRefIndex); + + private static native int[] typeStatus0(long isolateThread, long objectId, long[] typeIds); + + private static native void vmResume0(long isolateThread, long objectId, long[] ignoredThreadIds); + + private static native long[] vmSuspend0(long isolateThread, long objectId, long[] ignoredThreadIds); + } + + @SuppressWarnings("unused") + private static final class HSToNativeEndPoint { + + private static final BinaryMarshaller packetMarshaller; + private static final BinaryMarshaller stackFrameMarshaller; + private static final BinaryMarshaller throwableMarshaller; + static { + JNIConfig config = JDWPJNIConfig.getInstance(); + packetMarshaller = config.lookupMarshaller(Packet.class); + stackFrameMarshaller = config.lookupMarshaller(StackFrame.class); + throwableMarshaller = config.lookupMarshaller(Throwable.class); + } + + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_clearStepping0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void clearStepping(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long threadId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::clearStepping", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.clearStepping(threadId); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_currentWorkingDirectory0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static JString currentWorkingDirectory(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId) { + JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::currentWorkingDirectory", jniEnv); + try (JNIMethodScope sc = scope) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + String endPointResult = receiverObject.currentWorkingDirectory(); + scope.setObjectResult(JNIUtil.createHSString(jniEnv, endPointResult)); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + scope.setObjectResult(WordFactory.nullPointer()); + } + return scope.getObjectResult(); + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_dispatch0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static JByteArray dispatch(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, JByteArray marshalledData) { + JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::dispatch", jniEnv); + try (JNIMethodScope sc = scope) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + CCharPointer staticMarshallBuffer = StackValue.get(256); + int marshalledDataLength = JNIUtil.GetArrayLength(jniEnv, marshalledData); + CCharPointer marshallBuffer = marshalledDataLength <= 256 ? staticMarshallBuffer : UnmanagedMemory.malloc(marshalledDataLength); + try { + JNIUtil.GetByteArrayRegion(jniEnv, marshalledData, 0, marshalledDataLength, marshallBuffer); + BinaryInput marshalledParametersInput = BinaryInput.create(marshallBuffer, marshalledDataLength); + Packet packet = packetMarshaller.read(marshalledParametersInput); + Packet endPointResult = receiverObject.dispatch(packet); + int marshalledResultSizeEstimate = packetMarshaller.inferSize(endPointResult); + int marshallBufferLength = Math.max(256, marshalledDataLength); + try (CCharPointerBinaryOutput marshalledResultOutput = marshalledResultSizeEstimate > marshallBufferLength ? CCharPointerBinaryOutput.create(marshalledResultSizeEstimate) : BinaryOutput.create(marshallBuffer, marshallBufferLength, false)) { + packetMarshaller.write(marshalledResultOutput, endPointResult); + int marshalledResultPosition = marshalledResultOutput.getPosition(); + JByteArray marshalledResult = marshalledResultPosition <= marshalledDataLength ? marshalledData : JNIUtil.NewByteArray(jniEnv, marshalledResultPosition); + JNIUtil.SetByteArrayRegion(jniEnv, marshalledResult, 0, marshalledResultPosition, marshalledResultOutput.getAddress()); + scope.setObjectResult(marshalledResult); + } + } finally { + if (marshallBuffer != staticMarshallBuffer) { + UnmanagedMemory.free(marshallBuffer); + } + } + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + scope.setObjectResult(WordFactory.nullPointer()); + } + return scope.getObjectResult(); + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_fieldRefIdToIndex0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static int fieldRefIdToIndex(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long fieldRefId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::fieldRefIdToIndex", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + int endPointResult = receiverObject.fieldRefIdToIndex(fieldRefId); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_fieldRefIndexToId0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static long fieldRefIndexToId(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, int fieldRefIndex) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::fieldRefIndexToId", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + long endPointResult = receiverObject.fieldRefIndexToId(fieldRefIndex); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_getCurrentThis0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static long getCurrentThis(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::getCurrentThis", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + long endPointResult = receiverObject.getCurrentThis(); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_getSystemProperty0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static JString getSystemProperty(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, JString key) { + JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::getSystemProperty", jniEnv); + try (JNIMethodScope sc = scope) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + String endPointResult = receiverObject.getSystemProperty(JNIUtil.createString(jniEnv, key)); + scope.setObjectResult(JNIUtil.createHSString(jniEnv, endPointResult)); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + scope.setObjectResult(WordFactory.nullPointer()); + } + return scope.getObjectResult(); + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_getThreadFrames0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static JByteArray getThreadFrames(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long threadId) { + JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::getThreadFrames", jniEnv); + try (JNIMethodScope sc = scope) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + CCharPointer staticMarshallBuffer = StackValue.get(256); + StackFrame[] endPointResult = receiverObject.getThreadFrames(threadId); + int marshalledResultSizeEstimate = Integer.BYTES + (endPointResult != null && endPointResult.length > 0 ? endPointResult.length * stackFrameMarshaller.inferSize(endPointResult[0]) : 0); + try (CCharPointerBinaryOutput marshalledResultOutput = marshalledResultSizeEstimate > 256 ? CCharPointerBinaryOutput.create(marshalledResultSizeEstimate) : BinaryOutput.create(staticMarshallBuffer, 256, false)) { + if (endPointResult != null) { + marshalledResultOutput.writeInt(endPointResult.length); + for (StackFrame endPointResultElement : endPointResult) { + stackFrameMarshaller.write(marshalledResultOutput, endPointResultElement); + } + } else { + marshalledResultOutput.writeInt(-1); + } + int marshalledResultPosition = marshalledResultOutput.getPosition(); + JByteArray marshalledResult = JNIUtil.NewByteArray(jniEnv, marshalledResultPosition); + JNIUtil.SetByteArrayRegion(jniEnv, marshalledResult, 0, marshalledResultPosition, marshalledResultOutput.getAddress()); + scope.setObjectResult(marshalledResult); + } + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + scope.setObjectResult(WordFactory.nullPointer()); + } + return scope.getObjectResult(); + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_getThreadStatus0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static int getThreadStatus(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long threadId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::getThreadStatus", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + int endPointResult = receiverObject.getThreadStatus(threadId); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_isCurrentThreadVirtual0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static boolean isCurrentThreadVirtual(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::isCurrentThreadVirtual", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + boolean endPointResult = receiverObject.isCurrentThreadVirtual(); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return false; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_isEventEnabled0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static boolean isEventEnabled(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long threadId, int eventKind) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::isEventEnabled", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + boolean endPointResult = receiverObject.isEventEnabled(threadId, eventKind); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return false; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_methodRefIdToIndex0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static int methodRefIdToIndex(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long methodRefId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::methodRefIdToIndex", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + int endPointResult = receiverObject.methodRefIdToIndex(methodRefId); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_methodRefIndexToId0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static long methodRefIndexToId(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, int methodRefIndex) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::methodRefIndexToId", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + long endPointResult = receiverObject.methodRefIndexToId(methodRefIndex); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_setEventEnabled0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void setEventEnabled(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long threadId, int eventKind, boolean enable) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::setEventEnabled", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.setEventEnabled(threadId, eventKind, enable); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_setStepping0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void setStepping(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long threadId, int depth, int size) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::setStepping", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.setStepping(threadId, depth, size); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_setSteppingFromLocation0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void setSteppingFromLocation(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long threadId, int depth, int size, long methodId, int bci, int lineNumber) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::setSteppingFromLocation", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.setSteppingFromLocation(threadId, depth, size, methodId, bci, lineNumber); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_setThreadRequest0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void setThreadRequest(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, boolean start, boolean enable) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::setThreadRequest", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.setThreadRequest(start, enable); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_threadResume0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static long threadResume(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long suspendId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::threadResume", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + long endPointResult = receiverObject.threadResume(suspendId); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_threadSuspend0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static long threadSuspend(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long threadId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::threadSuspend", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + long endPointResult = receiverObject.threadSuspend(threadId); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_toggleBreakpoint0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void toggleBreakpoint(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long methodId, int bci, boolean enable) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::toggleBreakpoint", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.toggleBreakpoint(methodId, bci, enable); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_toggleMethodEnterEvent0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void toggleMethodEnterEvent(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long clazzId, boolean enable) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::toggleMethodEnterEvent", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.toggleMethodEnterEvent(clazzId, enable); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_toggleMethodExitEvent0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void toggleMethodExitEvent(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long clazzId, boolean enable) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::toggleMethodExitEvent", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.toggleMethodExitEvent(clazzId, enable); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_typeRefIdToIndex0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static int typeRefIdToIndex(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, long typeRefId) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::typeRefIdToIndex", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + int endPointResult = receiverObject.typeRefIdToIndex(typeRefId); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_typeRefIndexToId0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static long typeRefIndexToId(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, int typeRefIndex) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::typeRefIndexToId", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + long endPointResult = receiverObject.typeRefIndexToId(typeRefIndex); + return endPointResult; + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + return 0; + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_typeStatus0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static JIntArray typeStatus(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, JLongArray typeIds) { + JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::typeStatus", jniEnv); + try (JNIMethodScope sc = scope) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + int[] endPointResult = receiverObject.typeStatus(JNIUtil.createArray(jniEnv, typeIds)); + scope.setObjectResult(JNIUtil.createHSArray(jniEnv, endPointResult)); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + scope.setObjectResult(WordFactory.nullPointer()); + } + return scope.getObjectResult(); + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_vmResume0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static void vmResume(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, JLongArray ignoredThreadIds) { + try (JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::vmResume", jniEnv)) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + receiverObject.vmResume(JNIUtil.createArray(jniEnv, ignoredThreadIds)); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + } + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_HSToNativeJDWPBridgeGen_00024HSToNativeStartPoint_vmSuspend0", include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings({"try", "unused"}) + static JLongArray vmSuspend(JNIEnv jniEnv, JClass jniClazz, @IsolateThreadContext long isolateThread, long objectId, JLongArray ignoredThreadIds) { + JNIMethodScope scope = new JNIMethodScope("HSToNativeJDWPBridgeGen::vmSuspend", jniEnv); + try (JNIMethodScope sc = scope) { + JDWPBridge receiverObject = NativeObjectHandles.resolve(objectId, JDWPBridge.class); + long[] endPointResult = receiverObject.vmSuspend(JNIUtil.createArray(jniEnv, ignoredThreadIds)); + scope.setObjectResult(JNIUtil.createHSArray(jniEnv, endPointResult)); + } catch (Throwable e) { + ForeignException.forThrowable(e, throwableMarshaller).throwUsingJNI(jniEnv); + scope.setObjectResult(WordFactory.nullPointer()); + } + return scope.getObjectResult(); + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/IdTag.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/IdTag.java new file mode 100644 index 000000000000..b1d1939ec5eb --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/IdTag.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public enum IdTag { + ARBITRARY_REFERENCE, + INITIAL_IMAGE_HEAP_REFERENCE, + THREAD, + TYPE, + METHOD, + FIELD; + + // No valid ID should be equal to this e.g. type tags cannot be == TAG_MASK. + // Tag and index of this id are invalid. + public static final long INVALID_ID = -1L; + + private static final int TAG_BITS; + private static final int TAG_MASK; + private static final IdTag[] VALUES = IdTag.values(); + + static { + int size = VALUES.length; + int bits = 0; + while (size > 0) { + size >>= 1; + bits++; + } + TAG_BITS = bits; + TAG_MASK = (1 << TAG_BITS) - 1; + } + + public long toTaggedId(long index) { + assert index >= 0; + assert index == ((index << TAG_BITS) >>> TAG_BITS) : "index information lost"; + return index << TAG_BITS | this.ordinal(); + } + + public static long fromTaggedId(long id) { + long index = id >> TAG_BITS; + assert index >= 0; + return index; + } + + public static IdTag getTag(long id) { + return VALUES[(int) (id & TAG_MASK)]; + } + +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/InvokeOptions.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/InvokeOptions.java new file mode 100644 index 000000000000..a3f01b6afc3f --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/InvokeOptions.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public final class InvokeOptions { + + private InvokeOptions() { + } + + /** + * Otherwise, all threads started. + */ + public static final int INVOKE_SINGLE_THREADED = 0x01; + + /** + * Otherwise, normal virtual invoke (instance methods only). + */ + public static final int INVOKE_NONVIRTUAL = 0x02; + + public static boolean singleThreaded(int flags) { + return (flags & INVOKE_SINGLE_THREADED) != 0; + } + + public static boolean nonVirtual(int flags) { + return (flags & INVOKE_NONVIRTUAL) != 0; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWP.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWP.java new file mode 100644 index 000000000000..8239320d9f4c --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWP.java @@ -0,0 +1,925 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.core.util.VMError; + +// Checkstyle: stop method name check +// Checkstyle: stop field name check + +public interface JDWP { + + int VirtualMachine = 1; + int VirtualMachine_Version = 1; + int VirtualMachine_ClassesBySignature = 2; + int VirtualMachine_AllClasses = 3; + int VirtualMachine_AllThreads = 4; + int VirtualMachine_TopLevelThreadGroups = 5; + int VirtualMachine_Dispose = 6; + int VirtualMachine_IDSizes = 7; + int VirtualMachine_Suspend = 8; + int VirtualMachine_Resume = 9; + int VirtualMachine_Exit = 10; + int VirtualMachine_CreateString = 11; + int VirtualMachine_Capabilities = 12; + int VirtualMachine_ClassPaths = 13; + int VirtualMachine_DisposeObjects = 14; + int VirtualMachine_HoldEvents = 15; + int VirtualMachine_ReleaseEvents = 16; + int VirtualMachine_CapabilitiesNew = 17; + int VirtualMachine_RedefineClasses = 18; + int VirtualMachine_SetDefaultStratum = 19; + int VirtualMachine_AllClassesWithGeneric = 20; + int VirtualMachine_InstanceCounts = 21; + int VirtualMachine_AllModules = 22; + + default Packet VirtualMachine_Version(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.Version"); + } + + default Packet VirtualMachine_ClassesBySignature(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.ClassesBySignature"); + } + + default Packet VirtualMachine_AllClasses(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.AllClasses"); + } + + default Packet VirtualMachine_AllThreads(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.AllThreads"); + } + + default Packet VirtualMachine_TopLevelThreadGroups(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.TopLevelThreadGroups"); + } + + default Packet VirtualMachine_Dispose(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.Dispose"); + } + + default Packet VirtualMachine_IDSizes(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.IDSizes"); + } + + default Packet VirtualMachine_Suspend(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.Suspend"); + } + + default Packet VirtualMachine_Resume(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.Resume"); + } + + default Packet VirtualMachine_Exit(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.Exit"); + } + + default Packet VirtualMachine_CreateString(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.CreateString"); + } + + default Packet VirtualMachine_Capabilities(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.Capabilities"); + } + + default Packet VirtualMachine_ClassPaths(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.ClassPaths"); + } + + default Packet VirtualMachine_DisposeObjects(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.DisposeObjects"); + } + + default Packet VirtualMachine_HoldEvents(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.HoldEvents"); + } + + default Packet VirtualMachine_ReleaseEvents(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.ReleaseEvents"); + } + + default Packet VirtualMachine_CapabilitiesNew(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.CapabilitiesNew"); + } + + default Packet VirtualMachine_RedefineClasses(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.RedefineClasses"); + } + + default Packet VirtualMachine_SetDefaultStratum(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.SetDefaultStratum"); + } + + default Packet VirtualMachine_AllClassesWithGeneric(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.AllClassesWithGeneric"); + } + + default Packet VirtualMachine_InstanceCounts(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.InstanceCounts"); + } + + default Packet VirtualMachine_AllModules(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("VirtualMachine.AllModules"); + } + + int ReferenceType = 2; + int ReferenceType_Signature = 1; + int ReferenceType_ClassLoader = 2; + int ReferenceType_Modifiers = 3; + int ReferenceType_Fields = 4; + int ReferenceType_Methods = 5; + int ReferenceType_GetValues = 6; + int ReferenceType_SourceFile = 7; + int ReferenceType_NestedTypes = 8; + int ReferenceType_Status = 9; + int ReferenceType_Interfaces = 10; + int ReferenceType_ClassObject = 11; + int ReferenceType_SourceDebugExtension = 12; + int ReferenceType_SignatureWithGeneric = 13; + int ReferenceType_FieldsWithGeneric = 14; + int ReferenceType_MethodsWithGeneric = 15; + int ReferenceType_Instances = 16; + int ReferenceType_ClassFileVersion = 17; + int ReferenceType_ConstantPool = 18; + int ReferenceType_Module = 19; + + default Packet ReferenceType_Signature(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.Signature"); + } + + default Packet ReferenceType_ClassLoader(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.ClassLoader"); + } + + default Packet ReferenceType_Modifiers(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.Modifiers"); + } + + default Packet ReferenceType_Fields(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.Fields"); + } + + default Packet ReferenceType_Methods(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.Methods"); + } + + default Packet ReferenceType_GetValues(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.GetValues"); + } + + default Packet ReferenceType_SourceFile(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.SourceFile"); + } + + default Packet ReferenceType_NestedTypes(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.NestedTypes"); + } + + default Packet ReferenceType_Status(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.Status"); + } + + default Packet ReferenceType_Interfaces(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.Interfaces"); + } + + default Packet ReferenceType_ClassObject(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.ClassObject"); + } + + default Packet ReferenceType_SourceDebugExtension(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.SourceDebugExtension"); + } + + default Packet ReferenceType_SignatureWithGeneric(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.SignatureWithGeneric"); + } + + default Packet ReferenceType_FieldsWithGeneric(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.FieldsWithGeneric"); + } + + default Packet ReferenceType_Instances(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.Instances"); + } + + default Packet ReferenceType_MethodsWithGeneric(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.MethodsWithGeneric"); + } + + default Packet ReferenceType_ClassFileVersion(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.ClassFileVersion"); + } + + default Packet ReferenceType_ConstantPool(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.ConstantPool"); + } + + default Packet ReferenceType_Module(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ReferenceType.Module"); + } + + int ClassType = 3; + + int ClassType_Superclass = 1; + int ClassType_SetValues = 2; + int ClassType_InvokeMethod = 3; + int ClassType_NewInstance = 4; + + default Packet ClassType_Superclass(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ClassType.Superclass"); + } + + default Packet ClassType_SetValues(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ClassType.SetValues"); + } + + default Packet ClassType_InvokeMethod(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ClassType.InvokeMethod"); + } + + default Packet ClassType_NewInstance(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ClassType.NewInstance"); + } + + int ArrayType = 4; + + int ArrayType_NewInstance = 1; + + default Packet ArrayType_NewInstance(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ArrayType.NewInstance"); + } + + int InterfaceType = 5; + int InterfaceType_InvokeMethod = 1; + + default Packet InterfaceType_InvokeMethod(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("InterfaceType.InvokeMethod"); + } + + int Method = 6; + int Method_LineTable = 1; + int Method_VariableTable = 2; + int Method_Bytecodes = 3; + int Method_IsObsolete = 4; + int Method_VariableTableWithGeneric = 5; + + default Packet Method_LineTable(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("Method.LineTable"); + } + + default Packet Method_VariableTable(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("Method.VariableTable"); + } + + default Packet Method_Bytecodes(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("Method.Bytecodes"); + } + + default Packet Method_IsObsolete(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("Method.IsObsolete"); + } + + default Packet Method_VariableTableWithGeneric(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("Method.VariableTableWithGeneric"); + } + + int Field = 8; + int ObjectReference = 9; + + int ObjectReference_ReferenceType = 1; + int ObjectReference_GetValues = 2; + int ObjectReference_SetValues = 3; + int ObjectReference_MonitorInfo = 5; + int ObjectReference_InvokeMethod = 6; + int ObjectReference_DisableCollection = 7; + int ObjectReference_EnableCollection = 8; + int ObjectReference_IsCollected = 9; + int ObjectReference_ReferringObjects = 10; + + default Packet ObjectReference_ReferenceType(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.ReferenceType"); + } + + default Packet ObjectReference_GetValues(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.GetValues"); + } + + default Packet ObjectReference_SetValues(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.SetValues"); + } + + default Packet ObjectReference_MonitorInfo(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.MonitorInfo"); + } + + default Packet ObjectReference_InvokeMethod(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.InvokeMethod"); + } + + default Packet ObjectReference_DisableCollection(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.DisableCollection"); + } + + default Packet ObjectReference_EnableCollection(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.EnableCollection"); + } + + default Packet ObjectReference_IsCollected(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.IsCollected"); + } + + default Packet ObjectReference_ReferringObjects(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ObjectReference.ReferringObjects"); + } + + int StringReference = 10; + + int StringReference_Value = 1; + + default Packet StringReference_Value(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("StringReference.Value"); + } + + int ThreadReference = 11; + + int ThreadReference_Name = 1; + int ThreadReference_Suspend = 2; + int ThreadReference_Resume = 3; + int ThreadReference_Status = 4; + int ThreadReference_ThreadGroup = 5; + int ThreadReference_Frames = 6; + int ThreadReference_FrameCount = 7; + int ThreadReference_OwnedMonitors = 8; + int ThreadReference_CurrentContendedMonitor = 9; + int ThreadReference_Stop = 10; + int ThreadReference_Interrupt = 11; + int ThreadReference_SuspendCount = 12; + int ThreadReference_OwnedMonitorsStackDepthInfo = 13; + int ThreadReference_ForceEarlyReturn = 14; + int ThreadReference_IsVirtual = 15; + + default Packet ThreadReference_Name(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.Name"); + } + + default Packet ThreadReference_Suspend(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.Suspend"); + } + + default Packet ThreadReference_Resume(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.Resume"); + } + + default Packet ThreadReference_Status(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.Status"); + } + + default Packet ThreadReference_ThreadGroup(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.ThreadGroup"); + } + + default Packet ThreadReference_Frames(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.Frames"); + } + + default Packet ThreadReference_FrameCount(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.FrameCount"); + } + + default Packet ThreadReference_OwnedMonitors(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.OwnedMonitors"); + } + + default Packet ThreadReference_CurrentContendedMonitor(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.CurrentContendedMonitor"); + } + + default Packet ThreadReference_Stop(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.Stop"); + } + + default Packet ThreadReference_Interrupt(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.Interrupt"); + } + + default Packet ThreadReference_SuspendCount(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.SuspendCount"); + } + + default Packet ThreadReference_OwnedMonitorsStackDepthInfo(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.OwnedMonitorsStackDepthInfo"); + } + + default Packet ThreadReference_ForceEarlyReturn(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.ForceEarlyReturn"); + } + + default Packet ThreadReference_IsVirtual(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadReference.IsVirtual"); + } + + int ThreadGroupReference = 12; + + int ThreadGroupReference_Name = 1; + int ThreadGroupReference_Parent = 2; + int ThreadGroupReference_Children = 3; + + default Packet ThreadGroupReference_Name(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadGroupReference.Name"); + } + + default Packet ThreadGroupReference_Parent(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadGroupReference.Parent"); + } + + default Packet ThreadGroupReference_Children(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ThreadGroupReference.Children"); + } + + int ArrayReference = 13; + + int ArrayReference_Length = 1; + int ArrayReference_GetValues = 2; + int ArrayReference_SetValues = 3; + + default Packet ArrayReference_Length(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ArrayReferenc.Length"); + } + + default Packet ArrayReference_GetValues(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ArrayReferenc.GetValues"); + } + + default Packet ArrayReference_SetValues(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ArrayReferenc.SetValues"); + } + + int ClassLoaderReference = 14; + + int ClassLoaderReference_VisibleClasses = 1; + + default Packet ClassLoaderReference_VisibleClasses(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ClassLoaderReference.VisibleClasses"); + } + + int EventRequest = 15; + + int EventRequest_Set = 1; + int EventRequest_Clear = 2; + int EventRequest_ClearAllBreakpoints = 3; + + default Packet EventRequest_Set(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("EventRequest.Set"); + } + + default Packet EventRequest_Clear(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("EventRequest.Clear"); + } + + default Packet EventRequest_ClearAllBreakpoints(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("EventRequest.ClearAllBreakpoints"); + } + + int StackFrame = 16; + int StackFrame_GetValues = 1; + int StackFrame_SetValues = 2; + int StackFrame_ThisObject = 3; + int StackFrame_PopFrames = 4; + + default Packet StackFrame_GetValues(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("StackFrame.GetValues"); + } + + default Packet StackFrame_SetValues(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("StackFrame.SetValues"); + } + + default Packet StackFrame_ThisObject(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("StackFrame.ThisObject"); + } + + default Packet StackFrame_PopFrames(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("StackFrame.PopFrames"); + } + + int ClassObjectReference = 17; + + int ClassObjectReference_ReflectedType = 1; + + default Packet ClassObjectReference_ReflectedType(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ClassObjectReference.ReflectedType"); + } + + int ModuleReference = 18; + + int ModuleReference_Name = 1; + int ModuleReference_ClassLoader = 2; + + default Packet ModuleReference_Name(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ModuleReference.Name"); + } + + default Packet ModuleReference_ClassLoader(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("ModuleReference.ClassLoader"); + } + + int Event = 64; + int Event_Composite = 100; + + default Packet Event_Composite(@SuppressWarnings("unused") Packet packet) throws JDWPException { + throw VMError.unimplemented("Event.Composite"); + } + + default Packet dispatch(@SuppressWarnings("unused") Packet packet) throws JDWPException { + //@formatter:off + Packet response = switch (packet.commandSet()) { + case VirtualMachine -> switch (packet.command()) { + case VirtualMachine_Version -> VirtualMachine_Version(packet); + case VirtualMachine_ClassesBySignature -> VirtualMachine_ClassesBySignature(packet); + case VirtualMachine_AllClasses -> VirtualMachine_AllClasses(packet); + case VirtualMachine_AllThreads -> VirtualMachine_AllThreads(packet); + case VirtualMachine_TopLevelThreadGroups -> VirtualMachine_TopLevelThreadGroups(packet); + case VirtualMachine_Dispose -> VirtualMachine_Dispose(packet); + case VirtualMachine_IDSizes -> VirtualMachine_IDSizes(packet); + case VirtualMachine_Suspend -> VirtualMachine_Suspend(packet); + case VirtualMachine_Resume -> VirtualMachine_Resume(packet); + case VirtualMachine_Exit -> VirtualMachine_Exit(packet); + case VirtualMachine_CreateString -> VirtualMachine_CreateString(packet); + case VirtualMachine_Capabilities -> VirtualMachine_Capabilities(packet); + case VirtualMachine_ClassPaths -> VirtualMachine_ClassPaths(packet); + case VirtualMachine_DisposeObjects -> VirtualMachine_DisposeObjects(packet); + case VirtualMachine_HoldEvents -> VirtualMachine_HoldEvents(packet); + case VirtualMachine_ReleaseEvents -> VirtualMachine_ReleaseEvents(packet); + case VirtualMachine_CapabilitiesNew -> VirtualMachine_CapabilitiesNew(packet); + case VirtualMachine_RedefineClasses -> VirtualMachine_RedefineClasses(packet); + case VirtualMachine_SetDefaultStratum -> VirtualMachine_SetDefaultStratum(packet); + case VirtualMachine_AllClassesWithGeneric -> VirtualMachine_AllClassesWithGeneric(packet); + case VirtualMachine_InstanceCounts -> VirtualMachine_InstanceCounts(packet); + case VirtualMachine_AllModules -> VirtualMachine_AllModules(packet); + default -> + throw VMError.unimplemented("VirtualMachine command " + packet.command()); + }; + case ReferenceType -> switch (packet.command()) { + case ReferenceType_Signature -> ReferenceType_Signature(packet); + case ReferenceType_ClassLoader -> ReferenceType_ClassLoader(packet); + case ReferenceType_Modifiers -> ReferenceType_Modifiers(packet); + case ReferenceType_Fields -> ReferenceType_Fields(packet); + case ReferenceType_Methods -> ReferenceType_Methods(packet); + case ReferenceType_GetValues -> ReferenceType_GetValues(packet); + case ReferenceType_SourceFile -> ReferenceType_SourceFile(packet); + case ReferenceType_NestedTypes -> ReferenceType_NestedTypes(packet); + case ReferenceType_Status -> ReferenceType_Status(packet); + case ReferenceType_Interfaces -> ReferenceType_Interfaces(packet); + case ReferenceType_ClassObject -> ReferenceType_ClassObject(packet); + case ReferenceType_SourceDebugExtension -> ReferenceType_SourceDebugExtension(packet); + case ReferenceType_SignatureWithGeneric -> ReferenceType_SignatureWithGeneric(packet); + case ReferenceType_FieldsWithGeneric -> ReferenceType_FieldsWithGeneric(packet); + case ReferenceType_MethodsWithGeneric -> ReferenceType_MethodsWithGeneric(packet); + case ReferenceType_Instances -> ReferenceType_Instances(packet); + case ReferenceType_ClassFileVersion -> ReferenceType_ClassFileVersion(packet); + case ReferenceType_ConstantPool -> ReferenceType_ConstantPool(packet); + case ReferenceType_Module -> ReferenceType_Module(packet); + default -> + throw VMError.unimplemented("ReferenceType command " + packet.command()); + }; + case ClassType -> switch (packet.command()) { + case ClassType_Superclass -> ClassType_Superclass(packet); + case ClassType_SetValues -> ClassType_SetValues(packet); + case ClassType_InvokeMethod -> ClassType_InvokeMethod(packet); + case ClassType_NewInstance -> ClassType_NewInstance(packet); + default -> + throw VMError.unimplemented("ClassType command " + packet.command()); + }; + case ArrayType -> switch (packet.command()) { + case ArrayType_NewInstance -> ArrayType_NewInstance(packet); + default -> + throw VMError.unimplemented("ArrayType command " + packet.command()); + }; + case InterfaceType -> switch (packet.command()) { + case InterfaceType_InvokeMethod -> InterfaceType_InvokeMethod(packet); + default -> + throw VMError.unimplemented("InterfaceType command " + packet.command()); + }; + case Method -> switch (packet.command()) { + case Method_LineTable -> Method_LineTable(packet); + case Method_VariableTable -> Method_VariableTable(packet); + case Method_Bytecodes -> Method_Bytecodes(packet); + case Method_IsObsolete -> Method_IsObsolete(packet); + case Method_VariableTableWithGeneric -> Method_VariableTableWithGeneric(packet); + default -> + throw VMError.unimplemented("Method command " + packet.command()); + }; + case Field -> throw VMError.unimplemented("Field command " + packet.command()); + case ObjectReference -> switch (packet.command()) { + case ObjectReference_ReferenceType -> ObjectReference_ReferenceType(packet); + case ObjectReference_GetValues -> ObjectReference_GetValues(packet); + case ObjectReference_SetValues -> ObjectReference_SetValues(packet); + case ObjectReference_MonitorInfo -> ObjectReference_MonitorInfo(packet); + case ObjectReference_InvokeMethod -> ObjectReference_InvokeMethod(packet); + case ObjectReference_DisableCollection -> ObjectReference_DisableCollection(packet); + case ObjectReference_EnableCollection -> ObjectReference_EnableCollection(packet); + case ObjectReference_IsCollected -> ObjectReference_IsCollected(packet); + case ObjectReference_ReferringObjects -> ObjectReference_ReferringObjects(packet); + default -> + throw VMError.unimplemented("ObjectReference command " + packet.command()); + }; + case StringReference -> switch (packet.command()) { + case StringReference_Value -> StringReference_Value(packet); + default -> + throw VMError.unimplemented("StringReference command " + packet.command()); + }; + case ThreadReference -> switch (packet.command()) { + case ThreadReference_Name -> ThreadReference_Name(packet); + case ThreadReference_Suspend -> ThreadReference_Suspend(packet); + case ThreadReference_Resume -> ThreadReference_Resume(packet); + case ThreadReference_Status -> ThreadReference_Status(packet); + case ThreadReference_ThreadGroup -> ThreadReference_ThreadGroup(packet); + case ThreadReference_Frames -> ThreadReference_Frames(packet); + case ThreadReference_FrameCount -> ThreadReference_FrameCount(packet); + case ThreadReference_OwnedMonitors -> ThreadReference_OwnedMonitors(packet); + case ThreadReference_CurrentContendedMonitor -> ThreadReference_CurrentContendedMonitor(packet); + case ThreadReference_Stop -> ThreadReference_Stop(packet); + case ThreadReference_Interrupt -> ThreadReference_Interrupt(packet); + case ThreadReference_SuspendCount -> ThreadReference_SuspendCount(packet); + case ThreadReference_OwnedMonitorsStackDepthInfo -> ThreadReference_OwnedMonitorsStackDepthInfo(packet); + case ThreadReference_ForceEarlyReturn -> ThreadReference_ForceEarlyReturn(packet); + case ThreadReference_IsVirtual -> ThreadReference_IsVirtual(packet); + default -> + throw VMError.unimplemented("ThreadReference command " + packet.command()); + }; + case ThreadGroupReference -> switch (packet.command()) { + case ThreadGroupReference_Name -> ThreadGroupReference_Name(packet); + case ThreadGroupReference_Parent -> ThreadGroupReference_Parent(packet); + case ThreadGroupReference_Children -> ThreadGroupReference_Children(packet); + default -> + throw VMError.unimplemented("ThreadGroupReference command " + packet.command()); + }; + case ArrayReference -> switch (packet.command()) { + case ArrayReference_Length -> ArrayReference_Length(packet); + case ArrayReference_GetValues -> ArrayReference_GetValues(packet); + case ArrayReference_SetValues -> ArrayReference_SetValues(packet); + default -> + throw VMError.unimplemented("ArrayReference command " + packet.command()); + }; + case ClassLoaderReference -> switch (packet.command()) { + case ClassLoaderReference_VisibleClasses -> ClassLoaderReference_VisibleClasses(packet); + default -> + throw VMError.unimplemented("ClassLoaderReference command " + packet.command()); + }; + case EventRequest -> switch (packet.command()) { + case EventRequest_Set -> EventRequest_Set(packet); + case EventRequest_Clear -> EventRequest_Clear(packet); + case EventRequest_ClearAllBreakpoints -> EventRequest_ClearAllBreakpoints(packet); + default -> + throw VMError.unimplemented("EventRequest command " + packet.command()); + }; + case StackFrame -> switch (packet.command()) { + case StackFrame_GetValues -> StackFrame_GetValues(packet); + case StackFrame_SetValues -> StackFrame_SetValues(packet); + case StackFrame_ThisObject -> StackFrame_ThisObject(packet); + case StackFrame_PopFrames -> StackFrame_PopFrames(packet); + default -> + throw VMError.unimplemented("StackFrame command " + packet.command()); + }; + case ClassObjectReference -> switch (packet.command()) { + case ClassObjectReference_ReflectedType -> ClassObjectReference_ReflectedType(packet); + default -> + throw VMError.unimplemented("ClassObjectReference command " + packet.command()); + }; + case ModuleReference -> switch (packet.command()) { + case ModuleReference_Name -> ModuleReference_Name(packet); + case ModuleReference_ClassLoader -> ModuleReference_ClassLoader(packet); + default -> + throw VMError.unimplemented("ModuleReference command " + packet.command()); + }; + case Event -> switch (packet.command()) { + case Event_Composite -> Event_Composite(packet); + default -> + throw VMError.unimplemented("Event command " + packet.command()); + }; + default -> + throw VMError.unimplemented("CommandSet " + packet.commandSet()); + }; + //@formatter:on + return response; + } + + static String toString(int commandSet) { + //@formatter:off + return switch (commandSet) { + case VirtualMachine -> "VirtualMachine"; + case ReferenceType -> "ReferenceType"; + case ClassType -> "ClassType"; + case ArrayType -> "ArrayType"; + case InterfaceType -> "InterfaceType"; + case Method -> "Method"; + case Field -> "Field"; + case ObjectReference -> "ObjectReference"; + case StringReference -> "StringReference"; + case ThreadReference -> "ThreadReference"; + case ThreadGroupReference -> "ThreadGroupReference"; + case ArrayReference -> "ArrayReference"; + case ClassLoaderReference -> "ClassLoaderReference"; + case EventRequest -> "EventRequest"; + case StackFrame -> "StackFrame"; + case ClassObjectReference -> "ClassObjectReference"; + case ModuleReference -> "ModuleReference"; + case Event -> "Event"; + default -> unknown(commandSet); + }; + //@formatter:on + } + + static String toString(int commandSet, int command) { + //@formatter:off + return toString(commandSet) + "." + switch (commandSet) { + case VirtualMachine -> switch (command) { + case VirtualMachine_Version -> "Version"; + case VirtualMachine_ClassesBySignature -> "ClassesBySignature"; + case VirtualMachine_AllClasses -> "AllClasses"; + case VirtualMachine_AllThreads -> "AllThreads"; + case VirtualMachine_TopLevelThreadGroups -> "TopLevelThreadGroups"; + case VirtualMachine_Dispose -> "Dispose"; + case VirtualMachine_IDSizes -> "IDSizes"; + case VirtualMachine_Suspend -> "Suspend"; + case VirtualMachine_Resume -> "Resume"; + case VirtualMachine_Exit -> "Exit"; + case VirtualMachine_CreateString -> "CreateString"; + case VirtualMachine_Capabilities -> "Capabilities"; + case VirtualMachine_ClassPaths -> "ClassPaths"; + case VirtualMachine_DisposeObjects -> "DisposeObjects"; + case VirtualMachine_HoldEvents -> "HoldEvents"; + case VirtualMachine_ReleaseEvents -> "ReleaseEvents"; + case VirtualMachine_CapabilitiesNew -> "CapabilitiesNew"; + case VirtualMachine_RedefineClasses -> "RedefineClasses"; + case VirtualMachine_SetDefaultStratum -> "SetDefaultStratum"; + case VirtualMachine_AllClassesWithGeneric -> "AllClassesWithGeneric"; + case VirtualMachine_InstanceCounts -> "InstanceCounts"; + case VirtualMachine_AllModules -> "AllModules"; + default -> unknown(command); + }; + case ReferenceType -> switch (command) { + case ReferenceType_Signature -> "Signature"; + case ReferenceType_ClassLoader -> "ClassLoader"; + case ReferenceType_Modifiers -> "Modifiers"; + case ReferenceType_Fields -> "Fields"; + case ReferenceType_Methods -> "Methods"; + case ReferenceType_GetValues -> "GetValues"; + case ReferenceType_SourceFile -> "SourceFile"; + case ReferenceType_NestedTypes -> "NestedTypes"; + case ReferenceType_Status -> "Status"; + case ReferenceType_Interfaces -> "Interfaces"; + case ReferenceType_ClassObject -> "ClassObject"; + case ReferenceType_SourceDebugExtension -> "SourceDebugExtension"; + case ReferenceType_SignatureWithGeneric -> "SignatureWithGeneric"; + case ReferenceType_FieldsWithGeneric -> "FieldsWithGeneric"; + case ReferenceType_MethodsWithGeneric -> "MethodsWithGeneric"; + case ReferenceType_Instances -> "Instances"; + case ReferenceType_ClassFileVersion -> "ClassFileVersion"; + case ReferenceType_ConstantPool -> "ConstantPool"; + case ReferenceType_Module -> "Module"; + default -> unknown(command); + }; + case ClassType -> switch (command) { + case ClassType_Superclass -> "Superclass"; + case ClassType_SetValues -> "SetValues"; + case ClassType_InvokeMethod -> "InvokeMethod"; + case ClassType_NewInstance -> "NewInstance"; + default -> unknown(command); + }; + case ArrayType -> switch (command) { + case ArrayType_NewInstance -> "NewInstance"; + default -> unknown(command); + }; + case InterfaceType -> switch (command) { + case InterfaceType_InvokeMethod -> "InvokeMethod"; + default -> unknown(command); + }; + case Method -> switch (command) { + case Method_LineTable -> "LineTable"; + case Method_VariableTable -> "VariableTable"; + case Method_Bytecodes -> "Bytecodes"; + case Method_IsObsolete -> "IsObsolete"; + case Method_VariableTableWithGeneric -> "VariableTableWithGeneric"; + default -> unknown(command); + }; + case Field -> unknown(command); + case ObjectReference -> switch (command) { + case ObjectReference_ReferenceType -> "ReferenceType"; + case ObjectReference_GetValues -> "GetValues"; + case ObjectReference_SetValues -> "SetValues"; + case ObjectReference_MonitorInfo -> "MonitorInfo"; + case ObjectReference_InvokeMethod -> "InvokeMethod"; + case ObjectReference_DisableCollection -> "DisableCollection"; + case ObjectReference_EnableCollection -> "EnableCollection"; + case ObjectReference_IsCollected -> "IsCollected"; + case ObjectReference_ReferringObjects -> "ReferringObjects"; + default -> unknown(command); + }; + case StringReference -> switch (command) { + case StringReference_Value -> "Value"; + default -> unknown(command); + }; + case ThreadReference -> switch (command) { + case ThreadReference_Name -> "Name"; + case ThreadReference_Suspend -> "Suspend"; + case ThreadReference_Resume -> "Resume"; + case ThreadReference_Status -> "Status"; + case ThreadReference_ThreadGroup -> "ThreadGroup"; + case ThreadReference_Frames -> "Frames"; + case ThreadReference_FrameCount -> "FrameCount"; + case ThreadReference_OwnedMonitors -> "OwnedMonitors"; + case ThreadReference_CurrentContendedMonitor -> "CurrentContendedMonitor"; + case ThreadReference_Stop -> "Stop"; + case ThreadReference_Interrupt -> "Interrupt"; + case ThreadReference_SuspendCount -> "SuspendCount"; + case ThreadReference_OwnedMonitorsStackDepthInfo -> "OwnedMonitorsStackDepthInfo"; + case ThreadReference_ForceEarlyReturn -> "ForceEarlyReturn"; + case ThreadReference_IsVirtual -> "IsVirtual"; + default -> unknown(command); + }; + case ThreadGroupReference -> switch (command) { + case ThreadGroupReference_Name -> "Name"; + case ThreadGroupReference_Parent -> "Parent"; + case ThreadGroupReference_Children -> "Children"; + default -> unknown(command); + }; + case ArrayReference -> switch (command) { + case ArrayReference_Length -> "Length"; + case ArrayReference_GetValues -> "GetValues"; + case ArrayReference_SetValues -> "SetValues"; + default -> unknown(command); + }; + case ClassLoaderReference -> switch (command) { + case ClassLoaderReference_VisibleClasses -> "VisibleClasses"; + default -> unknown(command); + }; + case EventRequest -> switch (command) { + case EventRequest_Set -> "Set"; + case EventRequest_Clear -> "Clear"; + case EventRequest_ClearAllBreakpoints -> "ClearAllBreakpoints"; + default -> unknown(command); + }; + case StackFrame -> switch (command) { + case StackFrame_GetValues -> "GetValues"; + case StackFrame_SetValues -> "SetValues"; + case StackFrame_ThisObject -> "ThisObject"; + case StackFrame_PopFrames -> "PopFrames"; + default -> unknown(command); + }; + case ClassObjectReference -> switch (command) { + case ClassObjectReference_ReflectedType -> "ReflectedType"; + default -> unknown(command); + }; + case ModuleReference -> switch (command) { + case ModuleReference_Name -> "Name"; + case ModuleReference_ClassLoader -> "ClassLoader"; + default -> unknown(command); + }; + case Event -> switch (command) { + case Event_Composite -> "Composite"; + default -> unknown(command); + }; + default -> + unknown(command); + }; + //@formatter:on + } + + private static String unknown(int id) { + return ""; + } + + static byte readTag(Packet.Reader reader) throws JDWPException { + byte tag = (byte) reader.readByte(); + if (TagConstants.isValidTag(tag)) { + return tag; + } + throw JDWPException.raise(ErrorCode.INVALID_TAG); + } + +} +// Checkstyle: resume method name check +// Checkstyle: resume field name check diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPBridge.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPBridge.java new file mode 100644 index 000000000000..576bea12baf2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPBridge.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public interface JDWPBridge { + + int UNKNOWN_BCI = -1; + int UNINITIALIZED_LINE_NUMBER = -2; + + /* special thread id */ + long GLOBAL = 0; + + /** + * Enable/disable events in a specific thread or globally. An event is generated for a + * particular thread if it is enabled either at the thread or global levels. + * + * @param threadId threadId of application side, or GLOBAL to specify global scope + * @param eventKind one of the events in {@link EventKind} + * @param enable true/false to enable/disable the event + */ + void setEventEnabled(long threadId, int eventKind, boolean enable); + + /** + * Checks if an event is enabled for a specified thread or globally. An event is generated + * for a particular thread if it is enabled either at the thread or global levels. + * + * @param threadId threadId of application side, or GLOBAL to specify global scope + * @param eventKind one of the events in {@link EventKind} + */ + boolean isEventEnabled(long threadId, int eventKind); + + /** + * Enable/disable a breakpoint on the specified method and bytecode index. + * + * @throws IllegalArgumentException if the method doesn't have bytecodes, or the bytecode index + * is out of range, or if the bytecode index is not a valid b + */ + void toggleBreakpoint(long methodId, int bci, boolean enable); + + void toggleMethodEnterEvent(long clazzId, boolean enable); + + void toggleMethodExitEvent(long clazzId, boolean enable); + + /** + * Sets the stepping information associated with a thread. This enables stepping for the + * specified thread, enabling stepping is not enough, the stepping events must be enabled e.g. + * {@code Debugger.setEventEnabled(GLOBAL|threadId, EventKind.SINGLE_STEP, true} + * + * For line-stepping, the starting location can be set, otherwise, any location will raise the + * stepping event. + */ + void setSteppingFromLocation(long threadId, int depth, int size, long methodId, int bci, int lineNumber); + + default void setStepping(long threadId, int depth, int size) { + setSteppingFromLocation(threadId, depth, size, 0, UNKNOWN_BCI, UNINITIALIZED_LINE_NUMBER); + } + + /** + * Removes the stepping information associated with a thread. This cancels stepping for the + * current thread. + */ + void clearStepping(long threadId); + + Packet dispatch(Packet packet) throws JDWPException; + + /** + * Returns the ThreadStatus constant. + * + * @throws JDWPException when the {@code threadId} does not represent a {@code Thread}. + */ + int getThreadStatus(long threadId) throws JDWPException; + + long threadSuspend(long threadId); + + long threadResume(long suspendId); + + long[] vmSuspend(long[] ignoredThreadIds); + + void vmResume(long[] ignoredThreadIds); + + /** + * Request to send notifications about thread start/death. + * + * @param start true for thread start, false for thread death. + * @param enable true to enable the notifications, false to disable. + */ + void setThreadRequest(boolean start, boolean enable); + + String getSystemProperty(String key); + + long typeRefIndexToId(int typeRefIndex); + + long fieldRefIndexToId(int fieldRefIndex); + + long methodRefIndexToId(int methodRefIndex); + + int typeRefIdToIndex(long typeRefId); + + int fieldRefIdToIndex(long fieldRefId); + + int methodRefIdToIndex(long methodRefId); + + String currentWorkingDirectory(); + + int[] typeStatus(long... typeIds); + + StackFrame[] getThreadFrames(long threadId); + + boolean isCurrentThreadVirtual(); + + long getCurrentThis(); +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge.java new file mode 100644 index 000000000000..e1023cfc1099 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public interface JDWPEventHandlerBridge extends EventHandlerBridge { + + void spawnServer(String jdwpOptions, String additionalOptions, long isolate, long initialThreadId, long jdwpBridgeHandle, String metadataHashString, String metadataPath, boolean tracing); +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPException.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPException.java new file mode 100644 index 000000000000..6f1ab3cebb6d --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPException.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import java.io.Serial; + +public final class JDWPException extends RuntimeException { + + @Serial private static final long serialVersionUID = 5939656366374581700L; + + public ErrorCode getError() { + return error; + } + + private final ErrorCode error; + + JDWPException(ErrorCode error) { + this.error = error; + } + + public static JDWPException raise(ErrorCode error) { + throw new JDWPException(error); + } + + @Override + public String getMessage() { + return "(" + error.value() + ") " + error.getMessage(); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPJNIConfig.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPJNIConfig.java new file mode 100644 index 000000000000..7a6857c4f7d3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPJNIConfig.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryInput; +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryMarshaller; +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput; +import com.oracle.svm.jdwp.bridge.nativebridge.ForeignException; +import com.oracle.svm.jdwp.bridge.nativebridge.JNIConfig; +import com.oracle.svm.jdwp.bridge.nativebridge.MarshalledException; + +public class JDWPJNIConfig { + + private static final JNIConfig INSTANCE = createJNIConfig(); + + public static JNIConfig getInstance() { + return INSTANCE; + } + + private static JNIConfig createJNIConfig() { + JNIConfig.Builder builder = JNIConfig.newBuilder(); + builder.setAttachThreadAction(JDWPJNIConfig::attachCurrentThread); + + builder.registerMarshaller(Packet.class, new PacketMarshaller()); + EnumMarshaller errorCodeMarshaller = new EnumMarshaller<>(ErrorCode.class); + CustomThrowableMarshaller throwableMarshaller = new CustomThrowableMarshaller(new JDWPExceptionMarshaller(errorCodeMarshaller), new DefaultThrowableMarshaller()); + builder.registerMarshaller(Throwable.class, throwableMarshaller); + builder.registerMarshaller(StackFrame.class, new StackFrameMarshaller()); + + return builder.build(); + } + + private static final class PacketMarshaller implements BinaryMarshaller { + @Override + public void write(BinaryOutput output, Packet object) { + byte[] bytes = object.toByteArray(); + output.writeInt(bytes.length); + output.write(bytes, 0, bytes.length); + } + + @Override + public Packet read(BinaryInput input) { + int length = input.readInt(); + byte[] bytes = new byte[length]; + input.read(bytes, 0, bytes.length); + return UnmodifiablePacket.parseAndWrap(bytes); + } + } + + private static final class EnumMarshaller> implements BinaryMarshaller { + + private final E[] values; + + EnumMarshaller(Class enumClass) { + values = enumClass.getEnumConstants(); + if (values.length > Byte.MAX_VALUE) { + throw new IllegalArgumentException("Only " + Byte.MAX_VALUE + " enum constants are supported."); + } + } + + @Override + public E read(BinaryInput input) { + return values[input.readByte()]; + } + + @Override + public void write(BinaryOutput output, E object) { + output.writeByte(object.ordinal()); + } + + @Override + public int inferSize(E object) { + return 1; + } + } + + static final class CustomThrowableMarshaller implements BinaryMarshaller { + private final BinaryMarshaller jdwpExceptionMarshaller; + private final BinaryMarshaller fallbackThrowableMarshaller; + + private CustomThrowableMarshaller(BinaryMarshaller jdwpExceptionMarshaller, BinaryMarshaller fallbackThrowableMarshaller) { + this.jdwpExceptionMarshaller = jdwpExceptionMarshaller; + this.fallbackThrowableMarshaller = fallbackThrowableMarshaller; + } + + @Override + public void write(BinaryOutput output, Throwable object) { + boolean isJDWPException = object instanceof JDWPException; + output.writeBoolean(isJDWPException); + if (isJDWPException) { + jdwpExceptionMarshaller.write(output, (JDWPException) object); + } else { + fallbackThrowableMarshaller.write(output, object); + } + } + + @Override + public Throwable read(BinaryInput input) { + boolean isJDWPException = input.readBoolean(); + if (isJDWPException) { + return jdwpExceptionMarshaller.read(input); + } else { + return fallbackThrowableMarshaller.read(input); + } + } + + @Override + public int inferSize(Throwable object) { + if (object instanceof JDWPException jdwpException) { + return jdwpExceptionMarshaller.inferSize(jdwpException) + 1; + } else { + return fallbackThrowableMarshaller.inferSize(object) + 1; + } + } + } + + static final class DefaultThrowableMarshaller implements BinaryMarshaller { + + private static final int THROWABLE_SIZE_ESTIMATE = 1024; + private final BinaryMarshaller stackTraceMarshaller = JNIConfig.Builder.defaultStackTraceMarshaller(); + + @Override + public Throwable read(BinaryInput in) { + String foreignExceptionClassName = in.readUTF(); + String foreignExceptionMessage = (String) in.readTypedValue(); + StackTraceElement[] foreignExceptionStack = stackTraceMarshaller.read(in); + return new MarshalledException(foreignExceptionClassName, foreignExceptionMessage, ForeignException.mergeStackTrace(foreignExceptionStack)); + } + + @Override + public void write(BinaryOutput out, Throwable object) { + out.writeUTF(object instanceof MarshalledException ? ((MarshalledException) object).getForeignExceptionClassName() : object.getClass().getName()); + out.writeTypedValue(object.getMessage()); + stackTraceMarshaller.write(out, object.getStackTrace()); + } + + @Override + public int inferSize(Throwable object) { + // We don't use Throwable#getStackTrace as it allocates. + return THROWABLE_SIZE_ESTIMATE; + } + } + + private static final class JDWPExceptionMarshaller implements BinaryMarshaller { + + private final BinaryMarshaller stackTraceMarshaller; + private final BinaryMarshaller errorCodeMarshaller; + + private JDWPExceptionMarshaller(BinaryMarshaller errorCodeMarshaller) { + this.errorCodeMarshaller = errorCodeMarshaller; + this.stackTraceMarshaller = JNIConfig.Builder.defaultStackTraceMarshaller(); + } + + @Override + public void write(BinaryOutput output, JDWPException object) { + errorCodeMarshaller.write(output, object.getError()); + stackTraceMarshaller.write(output, object.getStackTrace()); + } + + @Override + public JDWPException read(BinaryInput input) { + ErrorCode error = errorCodeMarshaller.read(input); + StackTraceElement[] stackTraceElements = stackTraceMarshaller.read(input); + JDWPException jdwpException = new JDWPException(error); + jdwpException.setStackTrace(stackTraceElements); + return jdwpException; + } + + @Override + public int inferSize(JDWPException object) { + return errorCodeMarshaller.inferSize(object.getError()) + stackTraceMarshaller.inferSize(object.getStackTrace()); + } + } + + private static final class StackFrameMarshaller implements BinaryMarshaller { + + private static final int SIZE_OF_STACK_FRAME = Byte.BYTES + Long.BYTES + Long.BYTES + Integer.BYTES + Integer.BYTES; + + @Override + public void write(BinaryOutput output, StackFrame frame) { + output.writeByte(frame.typeTag()); + output.writeLong(frame.classId()); + output.writeLong(frame.methodId()); + output.writeInt(frame.bci()); + output.writeInt(frame.frameDepth()); + } + + @Override + public StackFrame read(BinaryInput input) { + byte typeTag = input.readByte(); + long classId = input.readLong(); + long methodId = input.readLong(); + int bci = input.readInt(); + int frameDepth = input.readInt(); + return new StackFrame(typeTag, classId, methodId, bci, frameDepth); + } + + @Override + public int inferSize(StackFrame frame) { + return SIZE_OF_STACK_FRAME; + } + } + + public static native long attachCurrentThread(long isolate); +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPNativeBridgeSupport.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPNativeBridgeSupport.java new file mode 100644 index 000000000000..ba7cb03afab5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/JDWPNativeBridgeSupport.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.jdwp.bridge.jniutils.NativeBridgeSupport; + +public class JDWPNativeBridgeSupport implements NativeBridgeSupport { + + @Override + public String getFeatureName() { + return "JDWPBridge"; + } + + @Override + public boolean isTracingEnabled(int level) { + return false; + } + + @Override + public void trace(String message) { + System.err.println("[" + getFeatureName() + "] " + message); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/Logger.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/Logger.java new file mode 100644 index 000000000000..d5d936c9b18b --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/Logger.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import java.io.PrintStream; +import java.util.function.Supplier; + +public final class Logger { + private final boolean enabled; + private final String prefix; + private final PrintStream sink; + + public Logger(boolean enabled, String prefix, PrintStream sink) { + this.enabled = enabled; + this.prefix = prefix; + this.sink = sink; + } + + public void log(Supplier s) { + if (enabled) { + log(s.get()); + } + } + + public void log(String message) { + if (enabled) { + sink.println(prefix + " " + message); + } + } + + public void log(Throwable throwable) { + if (enabled) { + throwable.printStackTrace(new PrintStream(sink) { + @Override + public void println(Object x) { + super.println(prefix + " " + x); + } + }); + } + } + + public void log(Throwable throwable, String message) { + if (enabled) { + log(message); + log(throwable); + } + } + + public boolean isLoggable() { + assert prefix != null; + return enabled; + } + + public void log(String messageSimpleFormat, Object... args) { + if (enabled) { + log(fmt(messageSimpleFormat, args)); + } + } + + /** + * Cheap alternative to {@link String#format(String, Object...)} that only provides simple + * modifiers. + */ + private static String fmt(String simpleFormat, Object... args) throws IllegalArgumentException { + StringBuilder sb = new StringBuilder(); + int index = 0; + int argIndex = 0; + while (index < simpleFormat.length()) { + char ch = simpleFormat.charAt(index++); + if (ch == '%') { + if (index >= simpleFormat.length()) { + throw new IllegalArgumentException("An unquoted '%' character cannot terminate a format specification"); + } + char specifier = simpleFormat.charAt(index++); + switch (specifier) { + case 's' -> { + if (argIndex >= args.length) { + throw new IllegalArgumentException("Too many format specifiers or not enough arguments"); + } + sb.append(args[argIndex++]); + } + case '%' -> sb.append('%'); + case 'n' -> sb.append(System.lineSeparator()); + default -> throw new IllegalArgumentException("Illegal format specifier: " + specifier); + } + } else { + sb.append(ch); + } + } + if (argIndex < args.length) { + throw new IllegalArgumentException("Not enough format specifiers or too many arguments"); + } + return sb.toString(); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/NativeToHSJDWPEventHandlerBridge.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/NativeToHSJDWPEventHandlerBridge.java new file mode 100644 index 000000000000..c61c62d809ad --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/NativeToHSJDWPEventHandlerBridge.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.jdwp.bridge.jniutils.HSObject; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObject; + +// Originally generated with nativebridge +// @GenerateNativeToHotSpotBridge(jniConfig = JDWPJNIConfig.class) +public abstract class NativeToHSJDWPEventHandlerBridge extends HSObject implements JDWPEventHandlerBridge { + + public NativeToHSJDWPEventHandlerBridge(JNIEnv env, JObject handle) { + super(env, handle); + } + + public static NativeToHSJDWPEventHandlerBridge createNativeToHS(JNIEnv env, JObject handle) { + return NativeToHSJDWPEventHandlerBridgeGen.createNativeToHS(env, handle); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/NativeToHSJDWPEventHandlerBridgeGen.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/NativeToHSJDWPEventHandlerBridgeGen.java new file mode 100644 index 000000000000..16cd5cb312e2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/NativeToHSJDWPEventHandlerBridgeGen.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +//Checkstyle: stop +// @formatter:off +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryMarshaller; +import com.oracle.svm.jdwp.bridge.nativebridge.ForeignException; +import com.oracle.svm.jdwp.bridge.nativebridge.JNIClassCache; +import com.oracle.svm.jdwp.bridge.nativebridge.JNIConfig; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JClass; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObject; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JValue; +import com.oracle.svm.jdwp.bridge.jniutils.JNICalls.JNIMethod; +import com.oracle.svm.jdwp.bridge.jniutils.JNIEntryPoint; +import com.oracle.svm.jdwp.bridge.jniutils.JNIMethodScope; +import com.oracle.svm.jdwp.bridge.jniutils.JNIUtil; +import org.graalvm.nativeimage.StackValue; + +/* Checkout README.md before modifying */ +final class NativeToHSJDWPEventHandlerBridgeGen { + + static NativeToHSJDWPEventHandlerBridge createNativeToHS(JNIEnv env, JObject handle) { + return new StartPoint(env, handle); + } + + private static final class StartPoint extends NativeToHSJDWPEventHandlerBridge { + + private static final BinaryMarshaller throwableMarshaller; + static { + JNIConfig config = JDWPJNIConfig.getInstance(); + throwableMarshaller = config.lookupMarshaller(Throwable.class); + } + + static final class JNIData { + + static JNIData cache_; + final JClass endPointClass; + final JNIMethod onEventAtMethod; + final JNIMethod onThreadDeathMethod; + final JNIMethod onThreadStartMethod; + final JNIMethod onVMDeathMethod; + final JNIMethod spawnServerMethod; + + JNIData(JNIEnv jniEnv) { + this.endPointClass = JNIClassCache.lookupClass(jniEnv, EndPoint.class); + this.onEventAtMethod = JNIMethod.findMethod(jniEnv, endPointClass, true, "onEventAt", "(Lcom/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge;JJBJIBJI)V"); + this.onThreadDeathMethod = JNIMethod.findMethod(jniEnv, endPointClass, true, "onThreadDeath", "(Lcom/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge;J)V"); + this.onThreadStartMethod = JNIMethod.findMethod(jniEnv, endPointClass, true, "onThreadStart", "(Lcom/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge;J)V"); + this.onVMDeathMethod = JNIMethod.findMethod(jniEnv, endPointClass, true, "onVMDeath", "(Lcom/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge;)V"); + this.spawnServerMethod = JNIMethod.findMethod(jniEnv, endPointClass, true, "spawnServer", "(Lcom/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge;Ljava/lang/String;Ljava/lang/String;JJJLjava/lang/String;Ljava/lang/String;Z)V"); + } + } + + final JNIData jniMethods_; + + StartPoint(JNIEnv env, JObject handle) { + super(env, handle); + JNIData localJNI = JNIData.cache_; + if (localJNI == null) { + localJNI = JNIData.cache_ = new JNIData(env); + } + this.jniMethods_ = localJNI; + } + + @Override + public void onEventAt(long threadId, long classId, byte typeTag, long methodId, int bci, byte resultTag, long resultPrimitiveOrId, int eventKindFlags) { + try { + JNIEnv jniEnv = JNIMethodScope.env(); + JValue jniArgs = StackValue.get(9, JValue.class); + jniArgs.addressOf(0).setJObject(this.getHandle()); + jniArgs.addressOf(1).setLong(threadId); + jniArgs.addressOf(2).setLong(classId); + jniArgs.addressOf(3).setByte(typeTag); + jniArgs.addressOf(4).setLong(methodId); + jniArgs.addressOf(5).setInt(bci); + jniArgs.addressOf(6).setByte(resultTag); + jniArgs.addressOf(7).setLong(resultPrimitiveOrId); + jniArgs.addressOf(8).setInt(eventKindFlags); + ForeignException.getJNICalls().callStaticVoid(jniEnv, jniMethods_.endPointClass, jniMethods_.onEventAtMethod, jniArgs); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } + } + + @Override + public void onThreadDeath(long threadId) { + try { + JNIEnv jniEnv = JNIMethodScope.env(); + JValue jniArgs = StackValue.get(2, JValue.class); + jniArgs.addressOf(0).setJObject(this.getHandle()); + jniArgs.addressOf(1).setLong(threadId); + ForeignException.getJNICalls().callStaticVoid(jniEnv, jniMethods_.endPointClass, jniMethods_.onThreadDeathMethod, jniArgs); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } + } + + @Override + public void onThreadStart(long threadId) { + try { + JNIEnv jniEnv = JNIMethodScope.env(); + JValue jniArgs = StackValue.get(2, JValue.class); + jniArgs.addressOf(0).setJObject(this.getHandle()); + jniArgs.addressOf(1).setLong(threadId); + ForeignException.getJNICalls().callStaticVoid(jniEnv, jniMethods_.endPointClass, jniMethods_.onThreadStartMethod, jniArgs); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } + } + + @Override + public void onVMDeath() { + try { + JNIEnv jniEnv = JNIMethodScope.env(); + JValue jniArgs = StackValue.get(1, JValue.class); + jniArgs.addressOf(0).setJObject(this.getHandle()); + ForeignException.getJNICalls().callStaticVoid(jniEnv, jniMethods_.endPointClass, jniMethods_.onVMDeathMethod, jniArgs); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } + } + + @Override + public void spawnServer(String jdwpOptions, String additionalOptions, long isolate, long initialThreadId, long jdwpBridgeHandle, String metadataHashString, String metadataPath, boolean tracing) { + try { + JNIEnv jniEnv = JNIMethodScope.env(); + JValue jniArgs = StackValue.get(9, JValue.class); + jniArgs.addressOf(0).setJObject(this.getHandle()); + jniArgs.addressOf(1).setJObject(JNIUtil.createHSString(jniEnv, jdwpOptions)); + jniArgs.addressOf(2).setJObject(JNIUtil.createHSString(jniEnv, additionalOptions)); + jniArgs.addressOf(3).setLong(isolate); + jniArgs.addressOf(4).setLong(initialThreadId); + jniArgs.addressOf(5).setLong(jdwpBridgeHandle); + jniArgs.addressOf(6).setJObject(JNIUtil.createHSString(jniEnv, metadataHashString)); + jniArgs.addressOf(7).setJObject(JNIUtil.createHSString(jniEnv, metadataPath)); + jniArgs.addressOf(8).setBoolean(tracing); + ForeignException.getJNICalls().callStaticVoid(jniEnv, jniMethods_.endPointClass, jniMethods_.spawnServerMethod, jniArgs); + } catch (ForeignException foreignException) { + throw foreignException.throwOriginalException(throwableMarshaller); + } + } + } + + private static class EndPoint { + + private static final BinaryMarshaller throwableMarshaller; + static { + JNIConfig config = JDWPJNIConfig.getInstance(); + throwableMarshaller = config.lookupMarshaller(Throwable.class); + } + + @SuppressWarnings({"unused"}) + @JNIEntryPoint + static void onEventAt(JDWPEventHandlerBridge receiverObject, long threadId, long classId, byte typeTag, long methodId, int bci, byte resultTag, long resultPrimitiveOrId, int eventKindFlags) { + try { + receiverObject.onEventAt(threadId, classId, typeTag, methodId, bci, resultTag, resultPrimitiveOrId, eventKindFlags); + } catch (Throwable e) { + throw ForeignException.forThrowable(e, throwableMarshaller); + } + } + + @SuppressWarnings({"unused"}) + @JNIEntryPoint + static void onThreadDeath(JDWPEventHandlerBridge receiverObject, long threadId) { + try { + receiverObject.onThreadDeath(threadId); + } catch (Throwable e) { + throw ForeignException.forThrowable(e, throwableMarshaller); + } + } + + @SuppressWarnings({"unused"}) + @JNIEntryPoint + static void onThreadStart(JDWPEventHandlerBridge receiverObject, long threadId) { + try { + receiverObject.onThreadStart(threadId); + } catch (Throwable e) { + throw ForeignException.forThrowable(e, throwableMarshaller); + } + } + + @SuppressWarnings({"unused"}) + @JNIEntryPoint + static void onVMDeath(JDWPEventHandlerBridge receiverObject) { + try { + receiverObject.onVMDeath(); + } catch (Throwable e) { + throw ForeignException.forThrowable(e, throwableMarshaller); + } + } + + @SuppressWarnings({"unused"}) + @JNIEntryPoint + static void spawnServer(JDWPEventHandlerBridge receiverObject, String jdwpOptions, String additionalOptions, long isolate, long initialThreadId, long jdwpBridgeHandle, String metadataHashString, String metadataPath, boolean tracing) { + try { + receiverObject.spawnServer(jdwpOptions, additionalOptions, isolate, initialThreadId, jdwpBridgeHandle, metadataHashString, metadataPath, tracing); + } catch (Throwable e) { + throw ForeignException.forThrowable(e, throwableMarshaller); + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/Packet.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/Packet.java new file mode 100644 index 000000000000..caf817d6d8a2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/Packet.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import java.nio.charset.StandardCharsets; + +public interface Packet { + + int NO_FLAGS = 0x0; + int REPLY = 0x80; + int REPLY_NO_ERROR = 0x0; + int HEADER_SIZE = 11; + + /** + * The size, in bytes, of JDWP packet, including the {@link #HEADER_SIZE header} or not. + * + * @param includeHeader specify if the {@link #HEADER_SIZE header bytes} are included or not + */ + default int length(boolean includeHeader) { + int packetLength = dataSize(); + if (includeHeader) { + packetLength += HEADER_SIZE; + } + return packetLength; + } + + /** + * The id field is used to uniquely identify each packet command/reply pair. A reply packet has + * the same id as the command packet to which it replies. This allows asynchronous commands and + * replies to be matched. The id field must be unique among all outstanding commands sent from + * one source. (Outstanding commands originating from the debugger may use the same id as + * outstanding commands originating from the target VM.) Other than that, there are no + * requirements on the allocation of id's. + * + * A simple monotonic counter should be adequate for most implementations. It will allow 2^32 + * unique outstanding packets and is the simplest implementation alternative. + */ + int id(); + + /** + * Flags are used to alter how any command is queued and processed and to tag command packets + * that originate from the target VM. There is currently one flag bits defined; future versions + * of the protocol may define additional flags. The reply bit {@code 0x80}, when set, indicates + * that this packet is a reply. + */ + byte flags(); + + default boolean isReply() { + return (flags() & REPLY) != 0; + } + + /** + * This field is used to indicate if the command packet that is being replied to was + * successfully processed. A value of zero indicates success, a non-zero value indicates an + * error. The error code returned may be specific to each command set/command, but it is often + * mapped to a JVM TI error code. + */ + short errorCode(); + + /** + * This field is useful as a means for grouping commands in a meaningful way. The Sun defined + * command sets are used to group commands by the interfaces they support in the JDI. For + * example, all commands that support the JDI VirtualMachine interface are grouped in a + * VirtualMachine command set. The command set space is roughly divided as follows: + *
    + *
  • 0 - 63 Sets of commands sent to the target VM
  • + *
  • 64 - 127 Sets of commands sent to the debugger
  • + *
  • 128 - 256 Vendor-defined commands and extensions.
  • + *
+ */ + byte commandSet(); + + /** + * This field identifies a particular command in a command set. This field, together with the + * command set field, is used to indicate how the command packet should be processed. More + * succinctly, they tell the receiver what to do. Specific commands are presented later in this + * document. + */ + byte command(); + + /** + * The size in bytes of the payload of this packet. + */ + int dataSize(); + + /** + * Returns the byte at the specified position in this packet's payload. + * + * @param index index of the bytes to return + * @throws IndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= dataSize()}) + */ + byte data(int index); + + default byte[] toByteArray() { + int length = this.length(true); + PacketWriterBuffer writer = new PacketWriterBufferImpl(length); + writer.writeInt(length); + writer.writeInt(id()); + writer.writeByte(flags()); + if (this.isReply()) { + writer.writeShort(errorCode()); + } else { + writer.writeByte(commandSet()); + writer.writeByte(command()); + } + int dataSize = this.dataSize(); + for (int i = 0; i < dataSize; ++i) { + writer.writeByte(data(i)); + } + return writer.toByteArray(); + } + + default Reader newDataReader() { + return new Reader() { + + private int position; + + @Override + public int readByte() { + return Byte.toUnsignedInt(data(position++)); + } + + @Override + public boolean isEndOfInput() { + return position >= dataSize(); + } + }; + } + + /** + * Utility to read data from JDWP packets in big-endian, avoids using JDK code to prevent the + * debugger to breakpoint itself. + */ + interface Reader { + + /** + * Reads returns a single byte as an unsigned int. To signal the end-of-input a negative + * integer can be returned or an exception thrown. + */ + int readByte(); + + default boolean readBoolean() { + return (readByte() != 0); + } + + default char readChar() { + return (char) readShort(); + } + + default short readShort() { + int b0 = readByte() & 0xff; + int b1 = readByte() & 0xff; + return (short) ((b0 << 8) | b1); + } + + default int readInt() { + int b0 = readByte() & 0xff; + int b1 = readByte() & 0xff; + int b2 = readByte() & 0xff; + int b3 = readByte() & 0xff; + return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3); + } + + default long readLong() { + long b0 = readByte() & 0xff; + long b1 = readByte() & 0xff; + long b2 = readByte() & 0xff; + long b3 = readByte() & 0xff; + long b4 = readByte() & 0xff; + long b5 = readByte() & 0xff; + long b6 = readByte() & 0xff; + long b7 = readByte() & 0xff; + return ((b0 << 56) | (b1 << 48) | (b2 << 40) | (b3 << 32) | (b4 << 24) | (b5 << 16) | (b6 << 8) | b7); + } + + default float readFloat() { + return Float.intBitsToFloat(readInt()); + } + + default double readDouble() { + return Double.longBitsToDouble(readLong()); + } + + default void readBytes(byte[] bytes) { + readBytes(bytes, 0, bytes.length); + } + + default void readBytes(byte[] bytes, int offset, int length) { + if (offset < 0 || length < 0 || offset > bytes.length - length) { + throw new ArrayIndexOutOfBoundsException(); + } + for (int i = 0; i < length; i++) { + bytes[offset + i] = (byte) (readByte() & 0xff); + } + } + + default String readString() { + int length = readInt(); + byte[] bytes = new byte[length]; + readBytes(bytes, 0, length); + return new String(bytes, StandardCharsets.UTF_8); + } + + default boolean isEndOfInput() { + return false; + } + } + + /** + * Utility to write payloads for JDWP packets in big-endian, avoids using JDK code to prevent + * the debugger to breakpoint itself. + */ + interface Writer { + /** + * Writes a single byte, represented by the least-significant 8-bits of the given integer, + * the remaining bits are ignored. + */ + void writeByte(int value); + + default void writeBoolean(boolean value) { + writeByte(value ? 1 : 0); + } + + default void writeChar(char value) { + writeShort((short) value); + } + + default void writeShort(short value) { + writeByte(value >> 8); + writeByte(value); + } + + default void writeInt(int value) { + writeByte(value >> 24); + writeByte(value >> 16); + writeByte(value >> 8); + writeByte(value); + } + + default void writeLong(long value) { + writeByte((int) (value >> 56)); + writeByte((int) (value >> 48)); + writeByte((int) (value >> 40)); + writeByte((int) (value >> 32)); + writeByte((int) (value >> 24)); + writeByte((int) (value >> 16)); + writeByte((int) (value >> 8)); + writeByte((int) value); + } + + default void writeFloat(float value) { + writeInt(Float.floatToRawIntBits(value)); + } + + default void writeDouble(double value) { + writeLong(Double.doubleToRawLongBits(value)); + } + + default void writeBytes(byte[] bytes) { + writeBytes(bytes, 0, bytes.length); + } + + default void writeBytes(byte[] bytes, int offset, int length) { + if (offset < 0 || length < 0 || offset > bytes.length - length) { + throw new ArrayIndexOutOfBoundsException(); + } + for (int i = 0; i < length; ++i) { + writeByte(bytes[offset + i]); + } + } + + default void writeString(String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + writeInt(bytes.length); + writeBytes(bytes); + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/PacketWriterBuffer.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/PacketWriterBuffer.java new file mode 100644 index 000000000000..6eb811405a42 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/PacketWriterBuffer.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +interface PacketWriterBuffer extends Packet.Writer { + int size(); + + byte byteAt(int index); + + default byte[] toByteArray() { + int length = size(); + byte[] result = new byte[length]; + for (int i = 0; i < length; i++) { + result[i] = this.byteAt(i); + } + return result; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/PacketWriterBufferImpl.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/PacketWriterBufferImpl.java new file mode 100644 index 000000000000..8145574097a2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/PacketWriterBufferImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import java.util.Arrays; + +final class PacketWriterBufferImpl implements PacketWriterBuffer { + private static final int INITIAL_CAPACITY = 64; + + PacketWriterBufferImpl() { + this(INITIAL_CAPACITY); + } + + PacketWriterBufferImpl(int initialCapacity) { + this.size = 0; + this.bytes = new byte[initialCapacity]; + } + + private int size; + private byte[] bytes; + + private void ensureCapacity(int capacity) { + if (capacity < bytes.length) { + return; + } + int newSize = size(); + if (newSize == 0) { + newSize = 1; + } + while (newSize < capacity) { + newSize = newSize * 2; + } + this.bytes = Arrays.copyOf(this.bytes, newSize); + } + + @Override + public void writeByte(int value) { + ensureCapacity(size + 1); + bytes[size++] = (byte) value; + } + + @Override + public int size() { + return size; + } + + @Override + public byte[] toByteArray() { + return Arrays.copyOf(this.bytes, size()); + } + + @Override + public byte byteAt(int index) { + if (index >= size()) { + throw new ArrayIndexOutOfBoundsException(); + } + return this.bytes[index]; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/README.md b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/README.md new file mode 100644 index 000000000000..f95f700db8fe --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/README.md @@ -0,0 +1,18 @@ +#### Why + +The JDWP implementation is split into two parties, (1) the resident which resides in the application image and (2) the server which is either a Native Image application itself or runs on HotSpot. Those parties communicate over JNI, and live in the same process. + +The boilerplate code for that communication was generated by the native bridge annotation processor. The same library (`sdk:nativebridge`) is also used for Polyglot Isolates in Truffle, and therefore is not part of a GraalVM but is obtained via maven or other means if needed. This means the JDWP implementation cannot use the same library, as the JDWP implementation is a component of GraalVM. + +One way to solve this would be shadowing, however that is potentially tricky as an annotation processor is involved. Since the interface isn't expected to change much anymore, we decided to check in the generated code instead and duplicate dependencies to it, see subpackages `jnuitils` and `nativebridge` in this project. + + +The sources of `jniutils` and `nativebridge` packages are a copy based on 199e453676902b74bba06581b259bb928d5f9a27 in the CE repository. + +### How to upgrade `NativeToHSJDWPEventHandlerBridgeGen` and `HSToNativeJDWPBridgeGen` + +1. Uncomment `@GenerateNativeToHotSpotBridge` annotation in `NativeToHSJDWPEventHandlerBridge` and `@GenerateHotSpotToNativeBridge` in `HSToNativeJDWPBridge` +2. Adapt imports in generated file accordingly. +3. There are a few cases where the fully qualified name to a class is used in a string, those must be updated as well. +4. Sync sources in `jniutils` and `nativebridge` if a newer revision is used. +5. Add `sdk:NATIVEBRIDGE_PROCESSOR` as `annotationProcessors` and `sdk:NATIVEBRIDGE` as dependency for this project, and build the project specifically with `mx build --target com.oracle.svm.jdwp.server`. diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ResidentJDWPFeatureEnabled.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ResidentJDWPFeatureEnabled.java new file mode 100644 index 000000000000..d84c4453f9ca --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ResidentJDWPFeatureEnabled.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import java.util.function.BooleanSupplier; + +import org.graalvm.nativeimage.ImageSingletons; + +/** + * A marker to detect the presence of the {@code ResidentJDWPFeature}. The + * {@code ResidentJDWPFeature} registers the {@link ResidentJDWPFeatureEnabled} instance into an + * {@link ImageSingletons}. The polyglot isolate code can use {@link ImageSingletons#contains(Class) + * ImageSingletons.contains(ResidentJDWPFeatureEnabled.class)} to prevent SubstrateVM from including + * methods that should not be reachable on the guest side. + */ +public final class ResidentJDWPFeatureEnabled implements BooleanSupplier { + + @Override + public boolean getAsBoolean() { + return isEnabled(); + } + + static boolean isEnabled() { + return ImageSingletons.contains(ResidentJDWPFeatureEnabled.class); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ServerJDWPFeatureEnabled.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ServerJDWPFeatureEnabled.java new file mode 100644 index 000000000000..f3103bce7b55 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/ServerJDWPFeatureEnabled.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import java.util.function.BooleanSupplier; + +import org.graalvm.nativeimage.ImageSingletons; + +/** + * A marker to detect the presence of the {@code ServerJDWPFeature}. The {@code ServerJDWPFeature} + * registers the {@link ServerJDWPFeatureEnabled} instance into an {@link ImageSingletons}. The + * polyglot isolate code can use {@link ImageSingletons#contains(Class) + * ImageSingletons.contains(ServerJDWPFeatureEnabled.class)} to prevent SubstrateVM from including + * methods that should not be reachable on the guest side. + */ +public final class ServerJDWPFeatureEnabled implements BooleanSupplier { + + @Override + public boolean getAsBoolean() { + return isEnabled(); + } + + static boolean isEnabled() { + return ImageSingletons.contains(ServerJDWPFeatureEnabled.class); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/StackFrame.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/StackFrame.java new file mode 100644 index 000000000000..8278a41ea308 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/StackFrame.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +/** + * Symbolic representation of a frame. This class does not contain the frame id, since this is a + * concept only on the server. + */ +public record StackFrame(byte typeTag, long classId, long methodId, int bci, int frameDepth) { + public StackFrame(byte typeTag, long classId, long methodId, int bci, int frameDepth) { + this.typeTag = typeTag; + this.classId = classId; + this.methodId = methodId; + assert frameDepth >= 0; + this.frameDepth = frameDepth; + this.bci = bci; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/SteppingControlConstants.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/SteppingControlConstants.java new file mode 100644 index 000000000000..8cc7c928a0de --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/SteppingControlConstants.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public class SteppingControlConstants { + /** + * Step into any newly pushed frames. + */ + public static final int STEP_INTO = 1; + /** + * Step over any newly pushed frames. + */ + public static final int STEP_OVER = 2; + /** + * Step out of the current frame. + */ + public static final int STEP_OUT = 3; + + /** + * Step to the next available location. + */ + public static final int STEP_MIN = -1; + /** + * Step to the next location on a different line. + */ + public static final int STEP_LINE = -2; +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/SymbolicRefs.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/SymbolicRefs.java new file mode 100644 index 000000000000..d4deabd7a27a --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/SymbolicRefs.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +public interface SymbolicRefs { + + long NULL = 0L; + + /** + * Returns the handle/id of the given "symbolic" type reference. If the given type is + * {@code null}, then {@code 0L} is returned. + * + * @throws IllegalArgumentException if the given reference is not part of the + * interpreter/metadata universe e.g. an arbitrary {@link ResolvedJavaType} + * instance. + */ + long toTypeRef(ResolvedJavaType resolvedJavaType); + + /** + * Returns the handle/id of the given "symbolic" field reference. If the given type is + * {@code null}, then {@code 0L} is returned. + * + * @throws IllegalArgumentException if the given reference is not part of the * + * interpreter/metadata universe e.g. an arbitrary {@link ResolvedJavaField} + * instance. + */ + long toFieldRef(ResolvedJavaField resolvedJavaField); + + /** + * Returns the handle/id of the given "symbolic" method reference associated. If the given type + * is {@code null}, then {@code 0L} is returned. + * + * @throws IllegalArgumentException if the given reference is not part of the * + * interpreter/metadata universe e.g. an arbitrary {@link ResolvedJavaMethod} + * instance. + */ + long toMethodRef(ResolvedJavaMethod resolvedJavaMethod); + + /** + * Returns the "symbolic" type reference associated for the given handle/id. If the given + * handle/id is {@code 0L}, then {@code null} is returned. + * + * @throws JDWPException with {@link ErrorCode#INVALID_CLASS} is handle/id does not refer to an + * instance {@link ResolvedJavaField} part of the interpreter/metadata universe. + * @throws JDWPException with {@link ErrorCode#INVALID_OBJECT} if was unloaded or garbage + * collected + */ + ResolvedJavaType toResolvedJavaType(long typeRefId) throws JDWPException; + + /** + * Returns the "symbolic" field reference associated for the given handle/id. If the given + * handle/id is {@code 0L}, then {@code null} is returned. + * + * @throws JDWPException with {@link ErrorCode#INVALID_FIELDID} is handle/id does not refer to + * an instance {@link ResolvedJavaField} part of the interpreter/metadata universe. + * @throws JDWPException with {@link ErrorCode#INVALID_OBJECT} if was unloaded or garbage + * collected + */ + ResolvedJavaField toResolvedJavaField(long fieldRefId) throws JDWPException; + + /** + * Returns the "symbolic" method reference associated for the given handle/id. If the given + * handle/id is {@code 0L}, then {@code null} is returned. + * + * @throws JDWPException with {@link ErrorCode#INVALID_METHODID} is handle/id does not refer to + * an instance {@link ResolvedJavaMethod} part of the interpreter/metadata universe + * @throws JDWPException with {@link ErrorCode#INVALID_OBJECT} if was unloaded or garbage + * collected + */ + ResolvedJavaMethod toResolvedJavaMethod(long methodRefId) throws JDWPException; +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/TagConstants.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/TagConstants.java new file mode 100644 index 000000000000..3b9a958adf5a --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/TagConstants.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.core.util.VMError; + +import jdk.vm.ci.meta.JavaKind; + +public final class TagConstants { + + public static final byte ARRAY = '['; + public static final byte BYTE = 'B'; + public static final byte CHAR = 'C'; + public static final byte OBJECT = 'L'; + public static final byte FLOAT = 'F'; + public static final byte DOUBLE = 'D'; + public static final byte INT = 'I'; + public static final byte LONG = 'J'; + public static final byte SHORT = 'S'; + public static final byte VOID = 'V'; + public static final byte BOOLEAN = 'Z'; + public static final byte STRING = 's'; + public static final byte THREAD = 't'; + public static final byte THREAD_GROUP = 'g'; + public static final byte CLASS_LOADER = 'l'; + public static final byte CLASS_OBJECT = 'c'; + + private TagConstants() { + } + + public static boolean isPrimitive(byte tag) { + return tag != OBJECT && + tag != STRING && + tag != ARRAY && + tag != THREAD && + tag != THREAD_GROUP && + tag != CLASS_OBJECT && + tag != CLASS_LOADER; + } + + public static byte getTagFromPrimitive(Object boxed) { + if (boxed instanceof Integer) { + return INT; + } + if (boxed instanceof Float) { + return FLOAT; + } + if (boxed instanceof Double) { + return DOUBLE; + } + if (boxed instanceof Long) { + return LONG; + } + if (boxed instanceof Byte) { + return BYTE; + } + if (boxed instanceof Short) { + return SHORT; + } + if (boxed instanceof Character) { + return CHAR; + } + if (boxed instanceof Boolean) { + return BOOLEAN; + } + throw new RuntimeException("Boxed object: " + boxed.getClass() + " is not a primitive"); + } + + public static Class getClassOfPrimitiveTag(int tag) { + return switch (tag) { + case INT -> Integer.TYPE; + case FLOAT -> Float.TYPE; + case DOUBLE -> Double.TYPE; + case LONG -> Long.TYPE; + case BYTE -> Byte.TYPE; + case SHORT -> Short.TYPE; + case CHAR -> Character.TYPE; + case BOOLEAN -> Boolean.TYPE; + default -> throw new IllegalArgumentException(Integer.toString(tag)); + }; + } + + public static boolean isValidTag(byte tag) { + return switch (tag) { + case ARRAY, BYTE, CHAR, OBJECT, FLOAT, DOUBLE, INT, LONG, SHORT, VOID, BOOLEAN, STRING, THREAD, THREAD_GROUP, CLASS_LOADER, CLASS_OBJECT -> true; + default -> false; + }; + } + + public static byte getTagFromClass(Class clazz) { + if (clazz.isArray()) { + return ARRAY; + } + if (clazz.isPrimitive()) { + if (clazz == Integer.TYPE) { + return INT; + } + if (clazz == Float.TYPE) { + return FLOAT; + } + if (clazz == Double.TYPE) { + return DOUBLE; + } + if (clazz == Long.TYPE) { + return LONG; + } + if (clazz == Byte.TYPE) { + return BYTE; + } + if (clazz == Short.TYPE) { + return SHORT; + } + if (clazz == Character.TYPE) { + return CHAR; + } + if (clazz == Boolean.TYPE) { + return BOOLEAN; + } + if (clazz == Void.TYPE) { + return VOID; + } + throw VMError.shouldNotReachHere("Unknown primitive class: " + clazz.getName()); + } else { + if (clazz == String.class) { + return STRING; + } + if (clazz == Class.class) { + return CLASS_OBJECT; + } + // These can be sub-classed, check for subtypes. + if (Thread.class.isAssignableFrom(clazz)) { + return THREAD; + } + if (ThreadGroup.class.isAssignableFrom(clazz)) { + return THREAD_GROUP; + } + if (ClassLoader.class.isAssignableFrom(clazz)) { + return CLASS_LOADER; + } + return OBJECT; + } + } + + public static byte getTagFromReference(Object ref) { + if (ref == null) { + return OBJECT; + } + return getTagFromClass(ref.getClass()); + } + + public static JavaKind tagToKind(byte tag) { + return switch (tag) { + case BYTE -> JavaKind.Byte; + case CHAR -> JavaKind.Char; + case FLOAT -> JavaKind.Float; + case DOUBLE -> JavaKind.Double; + case INT -> JavaKind.Int; + case LONG -> JavaKind.Long; + case SHORT -> JavaKind.Short; + case VOID -> JavaKind.Void; + case BOOLEAN -> JavaKind.Boolean; + case OBJECT, ARRAY, STRING, THREAD, THREAD_GROUP, CLASS_LOADER, CLASS_OBJECT -> JavaKind.Object; + default -> throw VMError.shouldNotReachHere("unreachable"); + }; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/TypeTag.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/TypeTag.java new file mode 100644 index 000000000000..62911ba6562c --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/TypeTag.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +import com.oracle.svm.core.util.VMError; + +import jdk.vm.ci.meta.ResolvedJavaType; + +public final class TypeTag { + + public static final byte CLASS = 1; + public static final byte INTERFACE = 2; + public static final byte ARRAY = 3; + + private TypeTag() { + } + + public static byte getKind(ResolvedJavaType type) { + if (type.isArray()) { + return ARRAY; + } else if (type.isInterface()) { + return INTERFACE; + } else { + VMError.guarantee(!type.isPrimitive()); + return CLASS; + } + } + + public static boolean isValidTypeTag(byte kind) { + return kind == CLASS || kind == INTERFACE || kind == ARRAY; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/UnmodifiablePacket.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/UnmodifiablePacket.java new file mode 100644 index 000000000000..1d59b82905a8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/UnmodifiablePacket.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public final class UnmodifiablePacket implements Packet { + + private final int id; + private final byte flags; + private final byte commandSet; + private final byte command; + private final short errorCode; + private final byte[] packetBytes; + + public static UnmodifiablePacket parseAndWrap(byte[] packetBytes) { + return new UnmodifiablePacket(packetBytes); + } + + private UnmodifiablePacket(byte[] packetBytes) { + if (packetBytes.length < HEADER_SIZE) { + throw new IllegalArgumentException(); + } + + Reader reader = new Reader() { + private int position; + + @Override + public int readByte() { + return Byte.toUnsignedInt(packetBytes[position++]); + } + + @Override + public boolean isEndOfInput() { + return position >= packetBytes.length; + } + }; + int length = reader.readInt(); + if (length != packetBytes.length) { + throw new IllegalArgumentException(); + } + + this.id = reader.readInt(); + this.flags = (byte) reader.readByte(); + + if (isReply()) { + this.errorCode = reader.readShort(); + this.commandSet = 0; + this.command = 0; + } else { + this.errorCode = REPLY_NO_ERROR; + this.commandSet = (byte) reader.readByte(); + this.command = (byte) reader.readByte(); + } + this.packetBytes = packetBytes; + } + + @Override + public int id() { + return id; + } + + @Override + public byte flags() { + return flags; + } + + @Override + public short errorCode() { + return errorCode; + } + + @Override + public byte commandSet() { + return commandSet; + } + + @Override + public byte command() { + return command; + } + + @Override + public int dataSize() { + return packetBytes.length - HEADER_SIZE; + } + + @Override + public byte data(int index) { + if (index < 0) { + throw new ArrayIndexOutOfBoundsException(); + } + return packetBytes[index + HEADER_SIZE]; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/WritablePacket.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/WritablePacket.java new file mode 100644 index 000000000000..57aae9f19714 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/WritablePacket.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge; + +public final class WritablePacket implements Packet { + + private int id; + private byte flags; + private short errorCode; + private byte commandSet; + private byte command; + + private PacketWriterBuffer dataWriter = new PacketWriterBufferImpl(); + + @Override + public int id() { + return id; + } + + public WritablePacket id(int newId) { + this.id = newId; + return this; + } + + @Override + public byte flags() { + return flags; + } + + public WritablePacket flags(byte newFlags) { + this.flags = newFlags; + return this; + } + + @Override + public short errorCode() { + return errorCode; + } + + public WritablePacket errorCode(short newErrorCode) { + this.errorCode = newErrorCode; + return this; + } + + public WritablePacket errorCode(ErrorCode newErrorCode) { + this.errorCode = (short) newErrorCode.value(); + return this; + } + + @Override + public byte commandSet() { + return commandSet; + } + + public WritablePacket commandSet(byte newCommandSet) { + this.commandSet = newCommandSet; + return this; + } + + @Override + public byte command() { + return command; + } + + public WritablePacket command(byte newCommand) { + this.command = newCommand; + return this; + } + + @Override + public int dataSize() { + return dataWriter.size(); + } + + @Override + public byte data(int index) { + return dataWriter.byteAt(index); + } + + public Packet.Writer dataWriter() { + return this.dataWriter; + } + + public WritablePacket dataWriter(PacketWriterBuffer newDataWriter) { + this.dataWriter = newDataWriter; + return this; + } + + public static WritablePacket newReplyTo(Packet pkt) { + return new WritablePacket().id(pkt.id()).flags((byte) REPLY).errorCode((byte) REPLY_NO_ERROR); + } + + public static WritablePacket commandPacket() { + // Note: Command packet uses the current Packet.uID (without incrementing it?). + return new WritablePacket().commandSet((byte) JDWP.Event).command((byte) JDWP.Event_Composite); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/HSObject.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/HSObject.java new file mode 100644 index 000000000000..31a665fadd34 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/HSObject.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +// Checkstyle: allow Class.getSimpleName +package com.oracle.svm.jdwp.bridge.jniutils; + +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.DeleteGlobalRef; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.DeleteWeakGlobalRef; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.NewGlobalRef; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.NewWeakGlobalRef; + +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObject; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JWeak; +import org.graalvm.word.WordFactory; + +/** + * Encapsulates a JNI handle to an object in the HotSpot heap. Depending on which constructor is + * used, the handle is either local to a {@link JNIMethodScope} and thus invalid once the scope + * exits or a global JNI handle that is only released sometime after the {@link HSObject} dies. + */ +public class HSObject { + + /** + * JNI handle to the HotSpot object. + */ + private final JObject handle; + + /** + * Cleaner for Global Reference. + */ + private final Cleaner cleaner; + + /** + * Link to next the next scope local object. The head of the list is in + * {@link JNIMethodScope#locals}. The handle of a scope local object is only valid for the + * lifetime of a {@link JNIMethodScope}. A self-reference (i.e. {@code this.next == this}) + * denotes an object whose {@link JNIMethodScope} has closed. + * + * This field is {@code null} for a non-scope local object. + */ + private HSObject next; + + /** + * Creates an object encapsulating a {@code handle} whose lifetime is determined by this object. + * The created {@link HSObject} uses a JNI global reference and does not allow duplicate JNI + * references. Use {@link #HSObject(JNIEnv, JObject, boolean, boolean)} to create + * {@link HSObject} using a JNI weak reference or allowing duplicate JNI references. + */ + public HSObject(JNIEnv env, JObject handle) { + this(env, handle, false, false); + } + + /** + * Creates an object encapsulating a {@code handle} whose lifetime is determined by this object. + * The created {@link HSObject} possibly allows duplicate JNI global handles. + */ + public HSObject(JNIEnv env, JObject handle, boolean allowGlobalDuplicates, boolean weak) { + cleanHandles(env); + if (checkingGlobalDuplicates(allowGlobalDuplicates)) { + checkNonExistingGlobalReference(env, handle); + } + String name = this.getClass().getName(); + this.handle = weak ? NewWeakGlobalRef(env, handle, name) : NewGlobalRef(env, handle, name); + cleaner = new Cleaner(this, this.handle, allowGlobalDuplicates, weak); + CLEANERS.add(cleaner); + next = null; + } + + private static boolean checkingGlobalDuplicates(boolean allowGlobalDuplicates) { + return !allowGlobalDuplicates && (assertionsEnabled() || JNIUtil.tracingAt(1)); + } + + /** + * Creates an object encapsulating a {@code handle} whose lifetime is limited to {@code scope}. + * Once {@code scope.close()} is called, any attempt to {@linkplain #getHandle() use} the handle + * will result in an {@link IllegalArgumentException}. + */ + public HSObject(JNIMethodScope scope, JObject handle) { + this.handle = handle; + next = scope.locals; + scope.locals = this; + cleaner = null; + } + + /** + * Invalidates the objects in the list starting at {@code head} such that subsequently calling + * {@link #getHandle()} on any of the objects results in an {@link IllegalArgumentException}. + * + * @return the number of objects in the list + */ + static int invalidate(HSObject head) { + HSObject o = head; + int count = 0; + while (o != null) { + HSObject next = o.next; + // Makes the handle now invalid. + o.next = o; + o = next; + count++; + } + return count; + } + + public final JObject getHandle() { + if (next == this) { + throw new IllegalArgumentException("Reclaimed JNI reference: " + this); + } + return handle; + } + + @Override + public String toString() { + return String.format("%s[0x%x]", getClass().getSimpleName(), handle.rawValue()); + } + + public final void release(JNIEnv env) { + if (cleaner != null) { + assert next == null || next == this; + this.next = this; + cleaner.clean(env); + } + } + + /** + * Processes {@link #CLEANERS_QUEUE} to release any handles whose objects are now unreachable. + */ + public static void cleanHandles(JNIEnv env) { + Cleaner cleaner; + while ((cleaner = (Cleaner) CLEANERS_QUEUE.poll()) != null) { + cleaner.clean(env); + } + } + + private static void checkNonExistingGlobalReference(JNIEnv env, JObject handle) { + for (Cleaner cleaner : CLEANERS) { + synchronized (cleaner) { + if (cleaner.handle.isNonNull() && JNIUtil.IsSameObject(env, handle, cleaner.handle)) { + throw new IllegalArgumentException("Global JNI handle already exists for object referenced by " + handle.rawValue()); + } + } + } + } + + private static boolean assertionsEnabled() { + boolean res = false; + assert (res = true) == true; + return res; + } + + /** + * Strong references to the {@link PhantomReference} objects. + */ + private static final Set CLEANERS = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** + * Queue into which a {@link Cleaner} is enqueued when its {@link HSObject} referent becomes + * unreachable. + */ + private static final ReferenceQueue CLEANERS_QUEUE = new ReferenceQueue<>(); + + private static final class Cleaner extends PhantomReference { + + private JObject handle; + private final boolean allowGlobalDuplicates; + private final boolean weak; + + Cleaner(HSObject referent, JObject handle, boolean allowGlobalDuplicates, boolean weak) { + super(referent, CLEANERS_QUEUE); + this.handle = handle; + this.allowGlobalDuplicates = allowGlobalDuplicates; + this.weak = weak; + } + + void clean(JNIEnv env) { + if (CLEANERS.remove(this)) { + if (checkingGlobalDuplicates(allowGlobalDuplicates)) { + synchronized (this) { + delete(env); + handle = WordFactory.nullPointer(); + } + } else { + delete(env); + } + } + } + + private void delete(JNIEnv env) { + if (weak) { + DeleteWeakGlobalRef(env, (JWeak) handle); + } else { + DeleteGlobalRef(env, handle); + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNI.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNI.java new file mode 100644 index 000000000000..97e677bfc23c --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNI.java @@ -0,0 +1,1111 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.jniutils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction.Transition; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.nativeimage.c.function.InvokeCFunctionPointer; +import org.graalvm.nativeimage.c.struct.CField; +import org.graalvm.nativeimage.c.struct.CPointerTo; +import org.graalvm.nativeimage.c.struct.CStruct; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CDoublePointer; +import org.graalvm.nativeimage.c.type.CFloatPointer; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.nativeimage.c.type.CLongPointer; +import org.graalvm.nativeimage.c.type.CShortPointer; +import org.graalvm.nativeimage.c.type.VoidPointer; +import org.graalvm.word.PointerBase; + +public final class JNI { + + public static final int JNI_OK = 0; + public static final int JNI_ERR = -1; /* unknown error */ + public static final int JNI_EDETACHED = -2; /* thread detached from the VM */ + public static final int JNI_EVERSION = -3; /* JNI version error */ + public static final int JNI_ENOMEM = -4; /* not enough memory */ + public static final int JNI_EEXIST = -5; /* VM already created */ + public static final int JNI_EINVAL = -6; /* invalid arguments */ + public static final int JNI_VERSION_10 = 0x000a0000; + + private JNI() { + throw new IllegalStateException("No instance allowed"); + } + + public interface JMethodID extends PointerBase { + } + + public interface JFieldID extends PointerBase { + } + + public interface JObject extends PointerBase { + } + + public interface JArray extends JObject { + int MODE_WRITE_RELEASE = 0; + int MODE_WRITE = 1; + int MODE_RELEASE = 2; + } + + public interface JBooleanArray extends JArray { + } + + public interface JByteArray extends JArray { + } + + public interface JCharArray extends JArray { + } + + public interface JShortArray extends JArray { + } + + public interface JIntArray extends JArray { + } + + public interface JLongArray extends JArray { + } + + public interface JFloatArray extends JArray { + } + + public interface JDoubleArray extends JArray { + } + + public interface JObjectArray extends JArray { + } + + public interface JClass extends JObject { + } + + public interface JString extends JObject { + } + + public interface JThrowable extends JObject { + } + + public interface JWeak extends JObject { + } + + /** + * Access to the {@code jvalue} JNI union. + * + *
+     * typedef union jvalue {
+     *    jboolean z;
+     *    jbyte    b;
+     *    jchar    c;
+     *    jshort   s;
+     *    jint     i;
+     *    jlong    j;
+     *    jfloat   f;
+     *    jdouble  d;
+     *    jobject  l;
+     * } jvalue;
+     * 
+ */ + @CContext(JNIHeaderDirectives.class) + @CStruct("jvalue") + public interface JValue extends PointerBase { + // @formatter:off + @CField("z") boolean getBoolean(); + @CField("b") byte getByte(); + @CField("c") char getChar(); + @CField("s") short getShort(); + @CField("i") int getInt(); + @CField("j") long getLong(); + @CField("f") float getFloat(); + @CField("d") double getDouble(); + @CField("l") JObject getJObject(); + + @CField("z") void setBoolean(boolean b); + @CField("b") void setByte(byte b); + @CField("c") void setChar(char ch); + @CField("s") void setShort(short s); + @CField("i") void setInt(int i); + @CField("j") void setLong(long l); + @CField("f") void setFloat(float f); + @CField("d") void setDouble(double d); + @CField("l") void setJObject(JObject obj); + // @formatter:on + + /** + * Gets JValue in an array of JValues pointed to by this object. + */ + JValue addressOf(int index); + } + + @CContext(JNIHeaderDirectives.class) + @CStruct(value = "JNIEnv_", addStructKeyword = true) + public interface JNIEnv extends PointerBase { + @CField("functions") + JNINativeInterface getFunctions(); + } + + @CPointerTo(JNIEnv.class) + public interface JNIEnvPointer extends PointerBase { + JNIEnv readJNIEnv(); + + void writeJNIEnv(JNIEnv env); + } + + @CContext(JNIHeaderDirectives.class) + @CStruct(value = "JNINativeInterface_", addStructKeyword = true) + public interface JNINativeInterface extends PointerBase { + + @CField("NewString") + NewString getNewString(); + + @CField("GetStringLength") + GetStringLength getGetStringLength(); + + @CField("GetStringChars") + GetStringChars getGetStringChars(); + + @CField("ReleaseStringChars") + ReleaseStringChars getReleaseStringChars(); + + @CField("NewStringUTF") + NewStringUTF8 getNewStringUTF(); + + @CField("GetStringUTFLength") + GetStringUTFLength getGetStringUTFLength(); + + @CField("GetStringUTFChars") + GetStringUTFChars getGetStringUTFChars(); + + @CField("ReleaseStringUTFChars") + ReleaseStringUTFChars getReleaseStringUTFChars(); + + @CField("GetArrayLength") + GetArrayLength getGetArrayLength(); + + @CField("NewLocalRef") + NewLocalRef getNewLocalRef(); + + @CField("NewObjectArray") + NewObjectArray getNewObjectArray(); + + @CField("NewBooleanArray") + NewBooleanArray getNewBooleanArray(); + + @CField("NewByteArray") + NewByteArray getNewByteArray(); + + @CField("NewCharArray") + NewCharArray getNewCharArray(); + + @CField("NewShortArray") + NewShortArray getNewShortArray(); + + @CField("NewIntArray") + NewIntArray getNewIntArray(); + + @CField("NewLongArray") + NewLongArray getNewLongArray(); + + @CField("NewFloatArray") + NewFloatArray getNewFloatArray(); + + @CField("NewDoubleArray") + NewDoubleArray getNewDoubleArray(); + + @CField("GetObjectArrayElement") + GetObjectArrayElement getGetObjectArrayElement(); + + @CField("SetObjectArrayElement") + SetObjectArrayElement getSetObjectArrayElement(); + + @CField("GetBooleanArrayElements") + GetBooleanArrayElements getGetBooleanArrayElements(); + + @CField("GetByteArrayElements") + GetByteArrayElements getGetByteArrayElements(); + + @CField("GetCharArrayElements") + GetCharArrayElements getGetCharArrayElements(); + + @CField("GetShortArrayElements") + GetShortArrayElements getGetShortArrayElements(); + + @CField("GetIntArrayElements") + GetIntArrayElements getGetIntArrayElements(); + + @CField("GetLongArrayElements") + GetLongArrayElements getGetLongArrayElements(); + + @CField("GetFloatArrayElements") + GetFloatArrayElements getGetFloatArrayElements(); + + @CField("GetDoubleArrayElements") + GetDoubleArrayElements getGetDoubleArrayElements(); + + @CField("ReleaseBooleanArrayElements") + ReleaseBooleanArrayElements getReleaseBooleanArrayElements(); + + @CField("ReleaseByteArrayElements") + ReleaseByteArrayElements getReleaseByteArrayElements(); + + @CField("ReleaseCharArrayElements") + ReleaseCharArrayElements getReleaseCharArrayElements(); + + @CField("ReleaseShortArrayElements") + ReleaseShortArrayElements getReleaseShortArrayElements(); + + @CField("ReleaseIntArrayElements") + ReleaseIntArrayElements getReleaseIntArrayElements(); + + @CField("ReleaseLongArrayElements") + ReleaseLongArrayElements getReleaseLongArrayElements(); + + @CField("ReleaseFloatArrayElements") + ReleaseFloatArrayElements getReleaseFloatArrayElements(); + + @CField("ReleaseDoubleArrayElements") + ReleaseDoubleArrayElements getReleaseDoubleArrayElements(); + + @CField("GetBooleanArrayRegion") + GetBooleanArrayRegion getGetBooleanArrayRegion(); + + @CField("GetByteArrayRegion") + GetByteArrayRegion getGetByteArrayRegion(); + + @CField("GetCharArrayRegion") + GetCharArrayRegion getGetCharArrayRegion(); + + @CField("GetShortArrayRegion") + GetShortArrayRegion getGetShortArrayRegion(); + + @CField("GetIntArrayRegion") + GetIntArrayRegion getGetIntArrayRegion(); + + @CField("GetLongArrayRegion") + GetLongArrayRegion getGetLongArrayRegion(); + + @CField("GetFloatArrayRegion") + GetFloatArrayRegion getGetFloatArrayRegion(); + + @CField("GetDoubleArrayRegion") + GetDoubleArrayRegion getGetDoubleArrayRegion(); + + @CField("SetBooleanArrayRegion") + SetBooleanArrayRegion getSetBooleanArrayRegion(); + + @CField("SetByteArrayRegion") + SetByteArrayRegion getSetByteArrayRegion(); + + @CField("SetCharArrayRegion") + SetCharArrayRegion getSetCharArrayRegion(); + + @CField("SetShortArrayRegion") + SetShortArrayRegion getSetShortArrayRegion(); + + @CField("SetIntArrayRegion") + SetIntArrayRegion getSetIntArrayRegion(); + + @CField("SetLongArrayRegion") + SetLongArrayRegion getSetLongArrayRegion(); + + @CField("SetFloatArrayRegion") + SetFloatArrayRegion getSetFloatArrayRegion(); + + @CField("SetDoubleArrayRegion") + SetDoubleArrayRegion getSetDoubleArrayRegion(); + + @CField("FindClass") + FindClass getFindClass(); + + @CField("DefineClass") + DefineClass getDefineClass(); + + @CField("IsSameObject") + IsSameObject getIsSameObject(); + + @CField("GetObjectClass") + GetObjectClass getGetObjectClass(); + + @CField("NewGlobalRef") + NewGlobalRef getNewGlobalRef(); + + @CField("DeleteGlobalRef") + DeleteGlobalRef getDeleteGlobalRef(); + + @CField("NewWeakGlobalRef") + NewWeakGlobalRef getNewWeakGlobalRef(); + + @CField("DeleteWeakGlobalRef") + DeleteWeakGlobalRef getDeleteWeakGlobalRef(); + + @CField("DeleteLocalRef") + DeleteLocalRef getDeleteLocalRef(); + + @CField("PushLocalFrame") + PushLocalFrame getPushLocalFrame(); + + @CField("PopLocalFrame") + PopLocalFrame getPopLocalFrame(); + + @CField("NewObjectA") + NewObjectA getNewObjectA(); + + @CField("GetStaticMethodID") + GetStaticMethodID getGetStaticMethodID(); + + @CField("GetMethodID") + GetMethodID getGetMethodID(); + + @CField("GetStaticFieldID") + GetStaticFieldID getGetStaticFieldID(); + + @CField("GetFieldID") + GetFieldID getGetFieldID(); + + @CField("CallStaticBooleanMethodA") + CallStaticBooleanMethodA getCallStaticBooleanMethodA(); + + @CField("CallStaticIntMethodA") + CallStaticIntMethodA getCallStaticIntMethodA(); + + @CField("CallStaticVoidMethodA") + CallStaticVoidMethodA getCallStaticVoidMethodA(); + + @CField("CallStaticObjectMethodA") + CallStaticObjectMethodA getCallStaticObjectMethodA(); + + @CField("CallStaticLongMethodA") + CallStaticLongMethodA getCallStaticLongMethodA(); + + @CField("CallObjectMethodA") + CallObjectMethodA getCallObjectMethodA(); + + @CField("CallVoidMethodA") + CallVoidMethodA getCallVoidMethodA(); + + @CField("CallBooleanMethodA") + CallBooleanMethodA getCallBooleanMethodA(); + + @CField("CallShortMethodA") + CallShortMethodA getCallShortMethodA(); + + @CField("CallIntMethodA") + CallIntMethodA getCallIntMethodA(); + + @CField("CallLongMethodA") + CallLongMethodA getCallLongMethodA(); + + @CField("CallDoubleMethodA") + CallDoubleMethodA getCallDoubleMethodA(); + + @CField("CallFloatMethodA") + CallFloatMethodA getCallFloatMethodA(); + + @CField("CallByteMethodA") + CallByteMethodA getCallByteMethodA(); + + @CField("CallCharMethodA") + CallCharMethodA getCallCharMethodA(); + + @CField("GetStaticObjectField") + GetStaticObjectField getGetStaticObjectField(); + + @CField("GetIntField") + GetIntField getGetIntField(); + + @CField("GetStaticBooleanField") + GetStaticBooleanField getGetStaticBooleanField(); + + @CField("SetStaticBooleanField") + SetStaticBooleanField getSetStaticBooleanField(); + + @CField("ExceptionCheck") + ExceptionCheck getExceptionCheck(); + + @CField("ExceptionOccurred") + ExceptionOccurred getExceptionOccurred(); + + @CField("ExceptionClear") + ExceptionClear getExceptionClear(); + + @CField("ExceptionDescribe") + ExceptionDescribe getExceptionDescribe(); + + @CField("Throw") + Throw getThrow(); + + @CField("GetObjectRefType") + GetObjectRefType getGetObjectRefType(); + + @CField("GetDirectBufferAddress") + GetDirectBufferAddress getGetDirectBufferAddress(); + + @CField("IsInstanceOf") + IsInstanceOf getIsInstanceOf(); + + @CField("GetJavaVM") + GetJavaVM getGetJavaVM(); + } + + @CContext(JNIHeaderDirectives.class) + @CStruct(value = "JavaVM_", addStructKeyword = true) + public interface JavaVM extends PointerBase { + @CField("functions") + JNIInvokeInterface getFunctions(); + } + + @CPointerTo(JavaVM.class) + public interface JavaVMPointer extends PointerBase { + JavaVM readJavaVM(); + + void writeJavaVM(JavaVM javaVM); + } + + @CContext(JNIHeaderDirectives.class) + @CStruct(value = "JavaVMAttachArgs", addStructKeyword = true) + public interface JavaVMAttachArgs extends PointerBase { + @CField("version") + int getVersion(); + + @CField("version") + void setVersion(int version); + + @CField("name") + CCharPointer getName(); + + @CField("name") + void setName(CCharPointer name); + + @CField("group") + JObject getGroup(); + + @CField("group") + void setGroup(JObject group); + } + + @CContext(JNIHeaderDirectives.class) + @CStruct(value = "JNIInvokeInterface_", addStructKeyword = true) + public interface JNIInvokeInterface extends PointerBase { + @CField("AttachCurrentThread") + AttachCurrentThread getAttachCurrentThread(); + + @CField("AttachCurrentThreadAsDaemon") + AttachCurrentThreadAsDaemon getAttachCurrentThreadAsDaemon(); + + @CField("DetachCurrentThread") + DetachCurrentThread getDetachCurrentThread(); + + @CField("GetEnv") + GetEnv getGetEnv(); + } + + public interface CallStaticIntMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + public interface CallStaticBooleanMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + boolean call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + public interface CallStaticVoidMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + public interface CallStaticObjectMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + JObject call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JObject callNoTransition(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + public interface CallStaticLongMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + long call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + public interface CallObjectMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObject object, JMethodID methodID, JValue args); + } + + public interface CallVoidMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface CallBooleanMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + boolean call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface CallShortMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + short call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface CallIntMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface CallLongMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + long call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface CallDoubleMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + double call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface CallFloatMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + float call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface CallByteMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + byte call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface CallCharMethodA extends CFunctionPointer { + @InvokeCFunctionPointer + char call(JNIEnv env, JObject o, JMethodID methodID, JValue args); + } + + public interface DeleteGlobalRef extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JObject gref); + } + + public interface DeleteWeakGlobalRef extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JWeak wref); + } + + public interface DeleteLocalRef extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JObject lref); + } + + public interface PushLocalFrame extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, int capacity); + } + + public interface PopLocalFrame extends CFunctionPointer { + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObject result); + } + + public interface ExceptionCheck extends CFunctionPointer { + @InvokeCFunctionPointer + boolean call(JNIEnv env); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + boolean callNoTransition(JNIEnv env); + } + + public interface ExceptionClear extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env); + } + + public interface ExceptionDescribe extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + void callNoTransition(JNIEnv env); + } + + public interface ExceptionOccurred extends CFunctionPointer { + @InvokeCFunctionPointer + JThrowable call(JNIEnv env); + } + + public interface FindClass extends CFunctionPointer { + @InvokeCFunctionPointer + JClass call(JNIEnv env, CCharPointer name); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JClass callNoTransition(JNIEnv env, CCharPointer name); + } + + public interface DefineClass extends CFunctionPointer { + @InvokeCFunctionPointer + JClass call(JNIEnv env, CCharPointer name, JObject loader, CCharPointer buf, long bufLen); + } + + public interface GetArrayLength extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JArray array); + } + + public interface GetBooleanArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + CCharPointer call(JNIEnv env, JBooleanArray array, JValue isCopy); + } + + public interface GetByteArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + CCharPointer call(JNIEnv env, JByteArray array, JValue isCopy); + } + + public interface GetCharArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + CShortPointer call(JNIEnv env, JCharArray array, JValue isCopy); + } + + public interface GetShortArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + CShortPointer call(JNIEnv env, JShortArray array, JValue isCopy); + } + + public interface GetIntArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + CIntPointer call(JNIEnv env, JIntArray array, JValue isCopy); + } + + public interface GetLongArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + CLongPointer call(JNIEnv env, JLongArray array, JValue isCopy); + } + + public interface GetFloatArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + CFloatPointer call(JNIEnv env, JFloatArray array, JValue isCopy); + } + + public interface GetDoubleArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + CDoublePointer call(JNIEnv env, JDoubleArray array, JValue isCopy); + } + + public interface GetMethodID extends CFunctionPointer { + @InvokeCFunctionPointer + JMethodID call(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JMethodID callNoTransition(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + } + + public interface GetObjectArrayElement extends CFunctionPointer { + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObjectArray array, int index); + } + + public interface GetObjectClass extends CFunctionPointer { + @InvokeCFunctionPointer + JClass call(JNIEnv env, JObject object); + } + + public interface GetObjectRefType extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JObject obj); + } + + public interface GetStaticMethodID extends CFunctionPointer { + @InvokeCFunctionPointer + JMethodID call(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JMethodID callNoTransition(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + } + + public interface GetStringChars extends CFunctionPointer { + @InvokeCFunctionPointer + CShortPointer call(JNIEnv env, JString string, JValue isCopy); + } + + public interface GetStringLength extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JString string); + } + + public interface GetStringUTFChars extends CFunctionPointer { + @InvokeCFunctionPointer + CCharPointer call(JNIEnv env, JString string, JValue isCopy); + } + + public interface GetStringUTFLength extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JString str); + } + + public interface IsSameObject extends CFunctionPointer { + @InvokeCFunctionPointer + boolean call(JNIEnv env, JObject ref1, JObject ref2); + } + + public interface NewBooleanArray extends CFunctionPointer { + @InvokeCFunctionPointer + JBooleanArray call(JNIEnv env, int len); + } + + public interface NewByteArray extends CFunctionPointer { + @InvokeCFunctionPointer + JByteArray call(JNIEnv env, int len); + } + + public interface NewCharArray extends CFunctionPointer { + @InvokeCFunctionPointer + JCharArray call(JNIEnv env, int len); + } + + public interface NewShortArray extends CFunctionPointer { + @InvokeCFunctionPointer + JShortArray call(JNIEnv env, int len); + } + + public interface NewIntArray extends CFunctionPointer { + @InvokeCFunctionPointer + JIntArray call(JNIEnv env, int len); + } + + public interface NewLongArray extends CFunctionPointer { + @InvokeCFunctionPointer + JLongArray call(JNIEnv env, int len); + } + + public interface NewFloatArray extends CFunctionPointer { + @InvokeCFunctionPointer + JFloatArray call(JNIEnv env, int len); + } + + public interface NewDoubleArray extends CFunctionPointer { + @InvokeCFunctionPointer + JDoubleArray call(JNIEnv env, int len); + } + + public interface NewGlobalRef extends CFunctionPointer { + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObject lobj); + } + + public interface NewWeakGlobalRef extends CFunctionPointer { + @InvokeCFunctionPointer + JWeak call(JNIEnv env, JObject lobj); + } + + public interface NewObjectA extends CFunctionPointer { + @InvokeCFunctionPointer + JObject call(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JObject callNoTransition(JNIEnv env, JClass clazz, JMethodID methodID, JValue args); + } + + public interface NewLocalRef extends CFunctionPointer { + @InvokeCFunctionPointer + JObject call(JNIEnv env, JObject obj); + } + + public interface NewObjectArray extends CFunctionPointer { + @InvokeCFunctionPointer + JObjectArray call(JNIEnv env, int len, JClass clazz, JObject init); + } + + public interface NewString extends CFunctionPointer { + @InvokeCFunctionPointer + JString call(JNIEnv env, CShortPointer unicode, int len); + } + + public interface NewStringUTF8 extends CFunctionPointer { + @InvokeCFunctionPointer + JString call(JNIEnv env, CCharPointer bytes); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + JString callNoTransition(JNIEnv env, CCharPointer bytes); + } + + public interface ReleaseBooleanArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JBooleanArray array, CCharPointer elems, int mode); + } + + public interface ReleaseByteArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JByteArray array, CCharPointer elems, int mode); + } + + public interface ReleaseCharArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JCharArray array, CShortPointer elems, int mode); + } + + public interface ReleaseShortArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JShortArray array, CShortPointer elems, int mode); + } + + public interface ReleaseIntArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JIntArray array, CIntPointer elems, int mode); + } + + public interface ReleaseLongArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JLongArray array, CLongPointer elems, int mode); + } + + public interface ReleaseFloatArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JFloatArray array, CFloatPointer elems, int mode); + } + + public interface ReleaseDoubleArrayElements extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JDoubleArray array, CDoublePointer elems, int mode); + } + + public interface GetBooleanArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JBooleanArray array, int start, int len, CCharPointer buf); + } + + public interface GetByteArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JByteArray array, int start, int len, CCharPointer buf); + } + + public interface GetCharArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JCharArray array, int start, int len, CShortPointer buf); + } + + public interface GetShortArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JShortArray array, int start, int len, CShortPointer buf); + } + + public interface GetIntArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JIntArray array, int start, int len, CIntPointer buf); + } + + public interface GetLongArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JLongArray array, int start, int len, CLongPointer buf); + } + + public interface GetFloatArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JFloatArray array, int start, int len, CFloatPointer buf); + } + + public interface GetDoubleArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JDoubleArray array, int start, int len, CDoublePointer buf); + } + + public interface SetBooleanArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JBooleanArray array, int start, int len, CCharPointer buf); + } + + public interface SetByteArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JByteArray array, int start, int len, CCharPointer buf); + } + + public interface SetCharArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JCharArray array, int start, int len, CShortPointer buf); + } + + public interface SetShortArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JShortArray array, int start, int len, CShortPointer buf); + } + + public interface SetIntArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JIntArray array, int start, int len, CIntPointer buf); + } + + public interface SetLongArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JLongArray array, int start, int len, CLongPointer buf); + } + + public interface SetFloatArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JFloatArray array, int start, int len, CFloatPointer buf); + } + + public interface SetDoubleArrayRegion extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JDoubleArray array, int start, int len, CDoublePointer buf); + } + + public interface ReleaseStringChars extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JString string, CShortPointer chars); + } + + public interface ReleaseStringUTFChars extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JString string, CCharPointer chars); + } + + public interface SetObjectArrayElement extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JObjectArray array, int index, JObject val); + } + + public interface Throw extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JThrowable throwable); + + @InvokeCFunctionPointer(transition = Transition.NO_TRANSITION) + int callNoTransition(JNIEnv env, JThrowable throwable); + } + + public interface GetDirectBufferAddress extends CFunctionPointer { + @InvokeCFunctionPointer + VoidPointer call(JNIEnv env, JObject buf); + } + + public interface IsInstanceOf extends CFunctionPointer { + @InvokeCFunctionPointer + boolean call(JNIEnv env, JObject o, JClass c); + } + + public interface GetStaticFieldID extends CFunctionPointer { + @InvokeCFunctionPointer + JFieldID call(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig); + } + + public interface GetFieldID extends CFunctionPointer { + @InvokeCFunctionPointer + JFieldID call(JNIEnv env, JClass c, CCharPointer name, CCharPointer sig); + } + + public interface GetStaticObjectField extends CFunctionPointer { + @InvokeCFunctionPointer + JObject call(JNIEnv env, JClass clazz, JFieldID fieldID); + } + + public interface GetIntField extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JObject o, JFieldID fieldId); + } + + public interface GetStaticBooleanField extends CFunctionPointer { + @InvokeCFunctionPointer + boolean call(JNIEnv env, JClass clazz, JFieldID fieldID); + } + + public interface SetStaticBooleanField extends CFunctionPointer { + @InvokeCFunctionPointer + void call(JNIEnv env, JClass clazz, JFieldID fieldID, boolean value); + } + + public interface GetJavaVM extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIEnv env, JavaVMPointer javaVMOut); + } + + public interface AttachCurrentThread extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JavaVM vm, JNIEnvPointer envOut, JavaVMAttachArgs args); + } + + public interface AttachCurrentThreadAsDaemon extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JavaVM vm, JNIEnvPointer envOut, JavaVMAttachArgs args); + } + + public interface DetachCurrentThread extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JavaVM vm); + } + + public interface GetEnv extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JavaVM vm, JNIEnvPointer envOut, int version); + } + + static class JNIHeaderDirectives implements CContext.Directives { + private static final String[] INCLUDES = {"jni.h", "jni_md.h"}; + + @Override + public boolean isInConfiguration() { + return ImageSingletons.contains(NativeBridgeSupport.class); + } + + @Override + public List getOptions() { + return Arrays.stream(findJNIHeaders()).map((p) -> "-I" + p.getParent()).collect(Collectors.toList()); + } + + @Override + public List getHeaderFiles() { + return Arrays.stream(findJNIHeaders()).map((p) -> '<' + p.toString() + '>').collect(Collectors.toList()); + } + + private static Path[] findJNIHeaders() { + Path javaHome = Paths.get(System.getProperty("java.home")); + Path includeFolder = javaHome.resolve("include"); + if (!Files.exists(includeFolder)) { + Path parent = javaHome.getParent(); + if (parent != null) { + javaHome = parent; + } + } + includeFolder = javaHome.resolve("include"); + if (!Files.exists(includeFolder)) { + throw new IllegalStateException("Cannot find 'include' folder in JDK."); + } + Path[] res = new Path[INCLUDES.length]; + try { + for (int i = 0; i < INCLUDES.length; i++) { + String include = INCLUDES[i]; + Optional includeFile = Files.find(includeFolder, 2, (p, attrs) -> include.equals(p.getFileName().toString())).findFirst(); + if (!includeFile.isPresent()) { + throw new IllegalStateException("Include: " + res[i] + " does not exist."); + } + res[i] = includeFile.get(); + } + return res; + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNICalls.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNICalls.java new file mode 100644 index 000000000000..b3c0c55c3c26 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNICalls.java @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.jniutils; + +import static com.oracle.svm.jdwp.bridge.jniutils.JNIExceptionWrapper.callGetClassName; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIExceptionWrapper.wrapAndThrowPendingJNIException; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.createString; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.trace; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.tracingAt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; + +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JClass; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JMethodID; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObject; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JValue; +import com.oracle.svm.jdwp.bridge.jniutils.JNIExceptionWrapper.ExceptionHandler; +import org.graalvm.word.WordFactory; + +/** + * Support for calling into HotSpot using JNI. In addition to calling a method using JNI, the + * {@code JNICalls} also perform JNI call tracing and exception handling. All JNI calls into HotSpot + * must use this support to correctly merge HotSpot and native image stack traces. + */ +public final class JNICalls { + + private static final JNICalls INSTANCE = new JNICalls(ExceptionHandler.DEFAULT); + + private static final ThreadLocal inTrace = ThreadLocal.withInitial(() -> false); + + private final ExceptionHandler exceptionHandler; + + private JNICalls(ExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + /** + * Returns a {@link JNICalls} instance with a default exception handler. The default exception + * handler rethrows any pending JNI exception as a {@link JNIExceptionWrapper}. + */ + public static JNICalls getDefault() { + return INSTANCE; + } + + /** + * Creates a new {@link JNICalls} instance with a custom exception handler. The given exception + * handler is used to handle pending JNI exceptions. + */ + public static JNICalls createWithExceptionHandler(ExceptionHandler handler) { + Objects.requireNonNull(handler, "Handler must be non null."); + return new JNICalls(handler); + } + + /** + * Performs a JNI call of a static void method. + */ + @JNICall + public void callStaticVoid(JNIEnv env, JClass clazz, JNIMethod method, JValue args) { + traceCall(env, clazz, method); + env.getFunctions().getCallStaticVoidMethodA().call(env, clazz, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + } + + /** + * Performs a JNI call of a static method returning {@code boolean}. + */ + @JNICall + public boolean callStaticBoolean(JNIEnv env, JClass clazz, JNIMethod method, JValue args) { + traceCall(env, clazz, method); + boolean res = env.getFunctions().getCallStaticBooleanMethodA().call(env, clazz, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a static method returning {@code long}. + */ + @JNICall + public long callStaticLong(JNIEnv env, JClass clazz, JNIMethod method, JValue args) { + traceCall(env, clazz, method); + long res = env.getFunctions().getCallStaticLongMethodA().call(env, clazz, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a static method returning {@code int}. + */ + @JNICall + public int callStaticInt(JNIEnv env, JClass clazz, JNIMethod method, JValue args) { + traceCall(env, clazz, method); + int res = env.getFunctions().getCallStaticIntMethodA().call(env, clazz, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a static method returning {@link Object}. + */ + @SuppressWarnings("unchecked") + @JNICall + public R callStaticJObject(JNIEnv env, JClass clazz, JNIMethod method, JValue args) { + traceCall(env, clazz, method); + JObject res = env.getFunctions().getCallStaticObjectMethodA().call(env, clazz, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return (R) res; + } + + /** + * Creates a new object instance using a given constructor. + */ + @JNICall + @SuppressWarnings("unchecked") + public R callNewObject(JNIEnv env, JClass clazz, JNIMethod constructor, JValue args) { + traceCall(env, clazz, constructor); + JObject res = env.getFunctions().getNewObjectA().call(env, clazz, constructor.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return (R) res; + } + + /** + * Performs a JNI call of a void method. + */ + @JNICall + public void callVoid(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + env.getFunctions().getCallVoidMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + } + + /** + * Performs a JNI call of a method returning {@link Object}. + */ + @JNICall + @SuppressWarnings("unchecked") + public R callJObject(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + JObject res = env.getFunctions().getCallObjectMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return (R) res; + } + + /** + * Performs a JNI call of a method returning {@code boolean}. + */ + @JNICall + public boolean callBoolean(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + boolean res = env.getFunctions().getCallBooleanMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a method returning {@code short}. + */ + @JNICall + public short callShort(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + short res = env.getFunctions().getCallShortMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a method returning {@code int}. + */ + @JNICall + public int callInt(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + int res = env.getFunctions().getCallIntMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a method returning {@code long}. + */ + @JNICall + public long callLong(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + long res = env.getFunctions().getCallLongMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a method returning {@code double}. + */ + @JNICall + public double callDouble(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + double res = env.getFunctions().getCallDoubleMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a method returning {@code float}. + */ + @JNICall + public float callFloat(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + float res = env.getFunctions().getCallFloatMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a method returning {@code byte}. + */ + @JNICall + public byte callByte(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + byte res = env.getFunctions().getCallByteMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + /** + * Performs a JNI call of a method returning {@code char}. + */ + @JNICall + public char callChar(JNIEnv env, JObject object, JNIMethod method, JValue args) { + traceCall(env, object, method); + char res = env.getFunctions().getCallCharMethodA().call(env, object, method.getJMethodID(), args); + wrapAndThrowPendingJNIException(env, exceptionHandler); + return res; + } + + private static void traceCall(JNIEnv env, JClass clazz, JNIMethod method) { + if (tracingAt(1)) { + traceCallImpl(env, clazz, method); + } + } + + private static void traceCall(JNIEnv env, JObject receiver, JNIMethod method) { + if (tracingAt(1)) { + // Intentionally does not use JNIUtil. The tracing JNI usage should not be traced. + traceCallImpl(env, env.getFunctions().getGetObjectClass().call(env, receiver), method); + } + } + + private static void traceCallImpl(JNIEnv env, JClass clazz, JNIMethod method) { + // The tracing performs JNI calls to obtain name of the HotSpot entry point class. + // This call must not be traced to prevent endless recursion. + if (!inTrace.get()) { + inTrace.set(true); + try { + trace(1, "%s->HS: %s::%s", + JNIUtil.getFeatureName(), + toSimpleName(createString(env, callGetClassName(env, clazz))), + method.getDisplayName()); + } finally { + inTrace.remove(); + } + } + } + + private static String toSimpleName(String fqn) { + int separatorIndex = fqn.lastIndexOf('.'); + return separatorIndex < 0 || separatorIndex + 1 == fqn.length() ? fqn : fqn.substring(separatorIndex + 1); + } + + /** + * Represents a JNI method. + */ + public interface JNIMethod { + + /** + * Returns a method JNI {@link JMethodID}. + */ + JMethodID getJMethodID(); + + /** + * Returns a method display name used for logging. + */ + String getDisplayName(); + + /** + * Finds a {@link JNIMethod} in the given {@link JClass clazz} with the given name and + * signature. If such a method does not exist throws {@link JNIExceptionWrapper} wrapping a + * {@link NoSuchMethodError}. + */ + static JNIMethod findMethod(JNIEnv env, JClass clazz, boolean staticMethod, String methodName, String methodSignature) { + return findMethod(env, clazz, staticMethod, true, methodName, methodSignature); + } + + /** + * Finds a {@link JNIMethod} in given {@link JClass clazz} with given name and signature. If + * such a method does not exist and {@code required} is {@code true}, it throws + * {@link JNIExceptionWrapper} wrapping a {@link NoSuchMethodError}. If {@code required} is + * {@code false} it clears the pending JNI exception and returns a + * {@link WordFactory#nullPointer() C NULL pointer}. + */ + static JNIMethod findMethod(JNIEnv env, JClass clazz, boolean staticMethod, boolean required, String methodName, String methodSignature) { + JMethodID methodID = JNIUtil.findMethod(env, clazz, staticMethod, required, methodName, methodSignature); + return methodID.isNull() ? null : new JNIMethod() { + @Override + public JMethodID getJMethodID() { + return methodID; + } + + @Override + public String getDisplayName() { + return methodName; + } + + @Override + public String toString() { + return methodName + methodSignature + "[0x" + Long.toHexString(methodID.rawValue()) + ']'; + } + }; + } + } + + /** + * Marker annotation for the helper methods for calling a method in HotSpot. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @interface JNICall { + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIEntryPoint.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIEntryPoint.java new file mode 100644 index 000000000000..f16807cd92bd --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIEntryPoint.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.jniutils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.graalvm.nativeimage.hosted.Feature; + +/** + * An annotation used to mark methods called by the JNI native interface. The annotation can be used + * by {@link Feature}s to register the annotated methods as JNI accessed. + */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JNIEntryPoint { +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIExceptionWrapper.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIExceptionWrapper.java new file mode 100644 index 000000000000..8fbddb40d6c6 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIExceptionWrapper.java @@ -0,0 +1,635 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +// Checkstyle: allow direct annotation access +package com.oracle.svm.jdwp.bridge.jniutils; + +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.ExceptionCheck; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.ExceptionClear; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.ExceptionDescribe; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.ExceptionOccurred; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.GetObjectClass; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.GetStaticMethodID; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.IsSameObject; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.NewGlobalRef; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.Throw; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.createArray; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.createHSArray; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.createHSString; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.createString; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.encodeMethodSignature; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.findClass; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.getBinaryName; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.getJVMCIClassLoader; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.trace; +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.tracingAt; +import static org.graalvm.nativeimage.c.type.CTypeConversion.toCString; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JByteArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JClass; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObject; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JString; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JThrowable; +import com.oracle.svm.jdwp.bridge.jniutils.JNICalls.JNICall; +import com.oracle.svm.jdwp.bridge.jniutils.JNICalls.JNIMethod; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.type.CTypeConversion; + +/** + * Wraps an exception thrown by a JNI call into HotSpot. If the exception propagates up to an native + * image entry point, the exception is re-thrown in HotSpot. + */ +@SuppressWarnings("serial") +public final class JNIExceptionWrapper extends RuntimeException { + + private static final String HS_ENTRYPOINTS_CLASS = "com.oracle.svm.jdwp.bridge.jniutils.JNIExceptionWrapperEntryPoints"; + private static final long serialVersionUID = 1L; + + private static final JNIMethodResolver CreateException = JNIMethodResolver.create("createException", Throwable.class, String.class); + private static final JNIMethodResolver GetClassName = JNIMethodResolver.create("getClassName", String.class, Class.class); + private static final JNIMethodResolver GetStackTrace = JNIMethodResolver.create("getStackTrace", byte[].class, Throwable.class); + private static final JNIMethodResolver GetThrowableMessage = JNIMethodResolver.create("getThrowableMessage", String.class, Throwable.class); + private static final JNIMethodResolver UpdateStackTrace = JNIMethodResolver.create("updateStackTrace", Throwable.class, Throwable.class, byte[].class); + + private static volatile JClass entryPointsClass; + + private final JThrowable throwableHandle; + private final boolean throwableRequiresStackTraceUpdate; + + private JNIExceptionWrapper(JNIEnv env, JThrowable throwableHandle) { + super(formatExceptionMessage(getClassName(env, throwableHandle), getMessage(env, throwableHandle))); + this.throwableHandle = throwableHandle; + this.throwableRequiresStackTraceUpdate = createMergedStackTrace(env); + } + + /** + * Re-throws this JNI exception in HotSpot after updating the stack trace to include the native + * image frames between the native image entry point and the call back to HotSpot that threw the + * original JNI exception. + */ + private void throwInHotSpot(JNIEnv env) { + JThrowable toThrow; + if (throwableRequiresStackTraceUpdate) { + toThrow = updateStackTrace(env, throwableHandle, getStackTrace()); + } else { + toThrow = throwableHandle; + } + Throw(env, toThrow); + } + + /** + * Creates a merged native image and HotSpot stack trace and updates this + * {@link JNIExceptionWrapper} stack trace to it. + * + * @return true if the stack trace needed a merge + */ + private boolean createMergedStackTrace(JNIEnv env) { + StackTraceElement[] hsStack = getJNIExceptionStackTrace(env, throwableHandle); + StackTraceElement[] mergedStack; + boolean res; + if (hsStack.length == 0 || containsJNIHostCall(hsStack)) { + mergedStack = hsStack; + res = false; + } else { + StackTraceElement[] nativeStack = getStackTrace(); + boolean originatedInHotSpot = !hsStack[0].isNativeMethod(); + mergedStack = mergeStackTraces(hsStack, nativeStack, 0, getIndexOfPropagateJNIExceptionFrame(nativeStack), originatedInHotSpot); + res = true; + } + setStackTrace(mergedStack); + return res; + } + + /** + * If there is a pending JNI exception, this method wraps it in a {@link JNIExceptionWrapper}, + * clears the pending exception and throws the {@link JNIExceptionWrapper} wrapper. The + * {@link JNIExceptionWrapper} message is composed of the JNI exception class name and the JNI + * exception message. For exception filtering or custom handling of JNI exceptions see + * {@link #wrapAndThrowPendingJNIException(JNIEnv, ExceptionHandler)}. + */ + public static void wrapAndThrowPendingJNIException(JNIEnv env) { + wrapAndThrowPendingJNIException(env, ExceptionHandler.DEFAULT); + } + + /** + * If there is a pending JNI exception, this method wraps it in a {@link JNIExceptionWrapper}, + * clears the pending exception and throws the {@link JNIExceptionWrapper} wrapper. The + * {@link JNIExceptionWrapper} message is composed of the JNI exception class name and the JNI + * exception message. + * + * @see ExceptionHandler + * + */ + public static void wrapAndThrowPendingJNIException(JNIEnv env, ExceptionHandler exceptionHandler) { + Objects.requireNonNull(exceptionHandler, "ExceptionHandler must be non null."); + if (ExceptionCheck(env)) { + JThrowable exception = ExceptionOccurred(env); + if (tracingAt(2) && exception.isNonNull()) { + ExceptionDescribe(env); + } + ExceptionClear(env); + exceptionHandler.handleException(new ExceptionHandlerContext(env, exception)); + } + } + + /** + * Throws an exception into HotSpot. + * + * If {@code original} is a {@link JNIExceptionWrapper} the wrapped JNI exception is thrown. + * + * Otherwise a new {@link RuntimeException} is thrown. The {@link RuntimeException} message is + * composed of {@code original.getClass().getName()} and {@code original.getMessage()}. The + * stack trace is result of merging the {@code original.getStackTrace()} with the current + * execution stack in HotSpot. + * + * @param env the {@link JNIEnv} + * @param original an exception to be thrown in HotSpot + */ + public static void throwInHotSpot(JNIEnv env, Throwable original) { + try { + trace(2, original); + if (original.getClass() == JNIExceptionWrapper.class) { + ((JNIExceptionWrapper) original).throwInHotSpot(env); + } else { + Throw(env, createHSException(env, original)); + } + } catch (Throwable t) { + // If something goes wrong when re-throwing the exception into HotSpot + // print the exception stack trace. + if (t instanceof ThreadDeath) { + throw t; + } else { + original.addSuppressed(t); + original.printStackTrace(); + } + } + } + + /** + * Crates an exception in HotSpot representing the given {@code original} exception. + * + * @param env the {@link JNIEnv} + * @param original an exception to be created in HotSpot + */ + public static JThrowable createHSException(JNIEnv env, Throwable original) { + JThrowable hsThrowable; + if (original instanceof JNIExceptionWrapper) { + JNIExceptionWrapper jniExceptionWrapper = (JNIExceptionWrapper) original; + hsThrowable = jniExceptionWrapper.throwableHandle; + if (jniExceptionWrapper.throwableRequiresStackTraceUpdate) { + hsThrowable = updateStackTrace(env, hsThrowable, jniExceptionWrapper.getStackTrace()); + } + } else { + String message = formatExceptionMessage(original.getClass().getName(), original.getMessage()); + JString hsMessage = createHSString(env, message); + hsThrowable = callCreateException(env, hsMessage); + StackTraceElement[] nativeStack = original.getStackTrace(); + if (nativeStack.length != 0) { + // Update stack trace only for exceptions which have stack trace. + // For exceptions which override fillInStackTrace merging stack traces only adds + // useless JNI calls. + StackTraceElement[] hsStack = getJNIExceptionStackTrace(env, hsThrowable); + StackTraceElement[] mergedStack = mergeStackTraces(hsStack, nativeStack, 1, + getIndexOfPropagateJNIExceptionFrame(nativeStack), false); + hsThrowable = updateStackTrace(env, hsThrowable, mergedStack); + } + } + return hsThrowable; + } + + /** + * Context for {@link ExceptionHandler}. + */ + public static final class ExceptionHandlerContext { + + private final JNIEnv env; + private final JThrowable throwable; + + ExceptionHandlerContext(JNIEnv env, JThrowable throwable) { + this.env = env; + this.throwable = throwable; + } + + /** + * Returns current thread JNIEnv. + */ + public JNIEnv getEnv() { + return env; + } + + /** + * Returns pending JNI exception. + */ + public JThrowable getThrowable() { + return throwable; + } + + /** + * Returns pending JNI exception class name. + */ + public String getThrowableClassName() { + return getClassName(env, throwable); + } + + /** + * Throws {@link JNIExceptionWrapper} for the pending JNI exception. + */ + public void throwJNIExceptionWrapper() { + throw new JNIExceptionWrapper(env, throwable); + } + } + + public interface ExceptionHandler { + + /** + * Default handler throwing {@link JNIExceptionWrapper} for the pending JNI exception. + */ + ExceptionHandler DEFAULT = new ExceptionHandler() { + @Override + public void handleException(ExceptionHandlerContext context) { + context.throwJNIExceptionWrapper(); + } + }; + + /** + * Creates an exception handler suppressing {@code allowedExceptions}. Other JNI exceptions + * are rethrown as {@link JNIExceptionWrapper}. + */ + @SafeVarargs + static ExceptionHandler allowExceptions(Class... allowedExceptions) { + return new ExceptionHandler() { + @Override + public void handleException(ExceptionHandlerContext context) { + JThrowable throwable = context.getThrowable(); + JNIEnv env = context.getEnv(); + JClass throwableClass = GetObjectClass(env, throwable); + boolean allowed = false; + for (Class allowedException : allowedExceptions) { + JClass allowedExceptionClass = findClass(env, getBinaryName(allowedException.getName())); + if (allowedExceptionClass.isNonNull() && IsSameObject(env, throwableClass, allowedExceptionClass)) { + allowed = true; + break; + } + } + if (!allowed) { + context.throwJNIExceptionWrapper(); + } + } + }; + } + + /** + * Handles the JNI pending exception. + */ + void handleException(ExceptionHandlerContext context); + } + + public static StackTraceElement[] mergeStackTraces( + StackTraceElement[] hotSpotStackTrace, + StackTraceElement[] nativeStackTrace, + boolean originatedInHotSpot) { + if (originatedInHotSpot) { + if (containsJNIHostCall(hotSpotStackTrace)) { + // Already merged + return hotSpotStackTrace; + } + } else { + if (containsJNIHostCall(nativeStackTrace)) { + // Already merged + return nativeStackTrace; + } + } + return mergeStackTraces(hotSpotStackTrace, nativeStackTrace, originatedInHotSpot ? 0 : getIndexOfTransitionToNativeFrame(hotSpotStackTrace), + getIndexOfPropagateJNIExceptionFrame(nativeStackTrace), originatedInHotSpot); + } + + /** + * Merges {@code hotSpotStackTrace} with {@code nativeStackTrace}. + * + * @param hotSpotStackTrace + * @param nativeStackTrace + * @param hotSpotStackStartIndex + * @param nativeStackStartIndex + * @param originatedInHotSpot + */ + private static StackTraceElement[] mergeStackTraces( + StackTraceElement[] hotSpotStackTrace, + StackTraceElement[] nativeStackTrace, + int hotSpotStackStartIndex, + int nativeStackStartIndex, + boolean originatedInHotSpot) { + int targetIndex = 0; + StackTraceElement[] merged = new StackTraceElement[hotSpotStackTrace.length - hotSpotStackStartIndex + nativeStackTrace.length - nativeStackStartIndex]; + boolean startingHotSpotFrame = true; + boolean startingnativeFrame = true; + boolean useHotSpotStack = originatedInHotSpot; + int hotSpotStackIndex = hotSpotStackStartIndex; + int nativeStackIndex = nativeStackStartIndex; + while (hotSpotStackIndex < hotSpotStackTrace.length || nativeStackIndex < nativeStackTrace.length) { + if (useHotSpotStack) { + while (hotSpotStackIndex < hotSpotStackTrace.length && (startingHotSpotFrame || !hotSpotStackTrace[hotSpotStackIndex].isNativeMethod())) { + startingHotSpotFrame = false; + merged[targetIndex++] = hotSpotStackTrace[hotSpotStackIndex++]; + } + startingHotSpotFrame = true; + } else { + useHotSpotStack = true; + } + while (nativeStackIndex < nativeStackTrace.length && (startingnativeFrame || !isJNIHostCall(nativeStackTrace[nativeStackIndex]))) { + startingnativeFrame = false; + merged[targetIndex++] = nativeStackTrace[nativeStackIndex++]; + } + startingnativeFrame = true; + } + return merged; + } + + /** + * Gets the stack trace from a JNI exception. + * + * @param env the {@link JNIEnv} + * @param throwableHandle the JNI exception to get the stack trace from. + * @return the stack trace + */ + private static StackTraceElement[] getJNIExceptionStackTrace(JNIEnv env, JObject throwableHandle) { + byte[] serializedStackTrace = createArray(env, (JByteArray) callGetStackTrace(env, throwableHandle)); + try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(serializedStackTrace))) { + int len = in.readInt(); + StackTraceElement[] res = new StackTraceElement[len]; + for (int i = 0; i < len; i++) { + String className = in.readUTF(); + String methodName = in.readUTF(); + String fileName = in.readUTF(); + fileName = fileName.isEmpty() ? null : fileName; + int lineNumber = in.readInt(); + res[i] = new StackTraceElement(className, methodName, fileName, lineNumber); + } + return res; + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + /** + * Determines if {@code stackTrace} contains a frame denoting a call into HotSpot. + */ + private static boolean containsJNIHostCall(StackTraceElement[] stackTrace) { + for (StackTraceElement e : stackTrace) { + if (isJNIHostCall(e)) { + return true; + } + } + return false; + } + + private static JThrowable updateStackTrace(JNIEnv env, JThrowable throwableHandle, StackTraceElement[] mergedStackTrace) { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(bout)) { + out.writeInt(mergedStackTrace.length); + for (int i = 0; i < mergedStackTrace.length; i++) { + StackTraceElement stackTraceElement = mergedStackTrace[i]; + out.writeUTF(stackTraceElement.getClassName()); + out.writeUTF(stackTraceElement.getMethodName()); + String fileName = stackTraceElement.getFileName(); + out.writeUTF(fileName == null ? "" : fileName); + out.writeInt(stackTraceElement.getLineNumber()); + } + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + return callUpdateStackTrace(env, throwableHandle, createHSArray(env, bout.toByteArray())); + } + + private static String getMessage(JNIEnv env, JThrowable throwableHandle) { + JString message = callGetThrowableMessage(env, throwableHandle); + return createString(env, message); + } + + private static String getClassName(JNIEnv env, JThrowable throwableHandle) { + JClass classHandle = GetObjectClass(env, throwableHandle); + JString className = callGetClassName(env, classHandle); + return createString(env, className); + } + + private static String formatExceptionMessage(String className, String message) { + StringBuilder builder = new StringBuilder(className); + if (message != null) { + builder.append(": ").append(message); + } + return builder.toString(); + } + + /** + * Gets the index of the first frame denoting the caller of + * {@link #wrapAndThrowPendingJNIException(JNIEnv)} or + * {@link #wrapAndThrowPendingJNIException(JNIEnv, ExceptionHandler)} in {@code stackTrace}. + * + * @return {@code 0} if no caller found + */ + private static int getIndexOfPropagateJNIExceptionFrame(StackTraceElement[] stackTrace) { + int state = 0; + for (int i = 0; i < stackTrace.length; i++) { + if (isStackFrame(stackTrace[i], JNIExceptionWrapper.class, "wrapAndThrowPendingJNIException")) { + state = 1; + } else if (state == 1) { + return i; + } + } + return 0; + } + + /** + * Gets the index of the first frame denoting the native method call. + * + * @return {@code 0} if no caller found + */ + private static int getIndexOfTransitionToNativeFrame(StackTraceElement[] stackTrace) { + for (int i = 0; i < stackTrace.length; i++) { + if (stackTrace[i].isNativeMethod()) { + return i; + } + } + return 0; + } + + private static boolean isStackFrame(StackTraceElement stackTraceElement, Class clazz, String methodName) { + return clazz.getName().equals(stackTraceElement.getClassName()) && methodName.equals(stackTraceElement.getMethodName()); + } + + // JNI calls + private static JThrowable callCreateException(JNIEnv env, JObject p0) { + JNI.JValue args = StackValue.get(1, JNI.JValue.class); + args.addressOf(0).setJObject(p0); + return JNICalls.getDefault().callStaticJObject(env, getEntryPoints(env), CreateException.resolve(env), args); + } + + private static T callUpdateStackTrace(JNIEnv env, JObject p0, JByteArray p1) { + JNI.JValue args = StackValue.get(2, JNI.JValue.class); + args.addressOf(0).setJObject(p0); + args.addressOf(1).setJObject(p1); + return JNICalls.getDefault().callStaticJObject(env, getEntryPoints(env), UpdateStackTrace.resolve(env), args); + } + + @SuppressWarnings("unchecked") + private static T callGetThrowableMessage(JNIEnv env, JObject p0) { + JNI.JValue args = StackValue.get(1, JNI.JValue.class); + args.addressOf(0).setJObject(p0); + return JNICalls.getDefault().callStaticJObject(env, getEntryPoints(env), GetThrowableMessage.resolve(env), args); + } + + @SuppressWarnings("unchecked") + static T callGetClassName(JNIEnv env, JObject p0) { + JNI.JValue args = StackValue.get(1, JNI.JValue.class); + args.addressOf(0).setJObject(p0); + return JNICalls.getDefault().callStaticJObject(env, getEntryPoints(env), GetClassName.resolve(env), args); + } + + @SuppressWarnings("unchecked") + private static T callGetStackTrace(JNIEnv env, JObject p0) { + JNI.JValue args = StackValue.get(1, JNI.JValue.class); + args.addressOf(0).setJObject(p0); + return JNICalls.getDefault().callStaticJObject(env, getEntryPoints(env), GetStackTrace.resolve(env), args); + } + + private static final class JNIMethodResolver implements JNIMethod { + + private final String methodName; + private final String methodSignature; + private volatile JNI.JMethodID methodId; + + private JNIMethodResolver(String methodName, String methodSignature) { + this.methodName = methodName; + this.methodSignature = methodSignature; + } + + JNIMethodResolver resolve(JNIEnv jniEnv) { + JNI.JMethodID res = methodId; + if (res.isNull()) { + JClass entryPointClass = getEntryPoints(jniEnv); + try (CTypeConversion.CCharPointerHolder name = toCString(methodName); CTypeConversion.CCharPointerHolder sig = toCString(methodSignature)) { + res = GetStaticMethodID(jniEnv, entryPointClass, name.get(), sig.get()); + if (res.isNull()) { + throw new InternalError("No such method: " + methodName); + } + methodId = res; + } + } + return this; + } + + @Override + public JNI.JMethodID getJMethodID() { + return methodId; + } + + @Override + public String getDisplayName() { + return methodName; + } + + static JNIMethodResolver create(String methodName, Class returnType, Class... parameterTypes) { + return new JNIMethodResolver(methodName, encodeMethodSignature(returnType, parameterTypes)); + } + } + + private static JClass getEntryPoints(JNIEnv env) { + JClass res = entryPointsClass; + if (res.isNull()) { + String binaryName = getBinaryName(HS_ENTRYPOINTS_CLASS); + JClass entryPoints = findClass(env, binaryName); + if (entryPoints.isNull()) { + // Clear the exception and try to load the entry points class using JVMCI + // classloader. + ExceptionClear(env); + JObject classLoader = getJVMCIClassLoader(env); + if (classLoader.isNonNull()) { + entryPoints = findClass(env, classLoader, binaryName); + } + } + if (entryPoints.isNull()) { + // Here we cannot use JNIExceptionWrapper. + // We failed to load HostSpot entry points for it. + ExceptionClear(env); + throw new InternalError("Failed to load " + HS_ENTRYPOINTS_CLASS); + } + synchronized (JNIExceptionWrapper.class) { + res = entryPointsClass; + if (res.isNull()) { + res = NewGlobalRef(env, entryPoints, "Class<" + HS_ENTRYPOINTS_CLASS + ">"); + entryPointsClass = res; + } + } + } + return res; + } + + /** + * Determines if {@code frame} is for a method denoting a call into HotSpot. + */ + private static boolean isJNIHostCall(StackTraceElement frame) { + return JNI_TRANSITION_CLASS.equals(frame.getClassName()) && JNI_TRANSITION_METHODS.contains(frame.getMethodName()); + } + + /** + * Names of the methods in the {@link JNICalls} class annotated by the {@link JNICall}. + */ + private static final Set JNI_TRANSITION_METHODS; + private static final String JNI_TRANSITION_CLASS; + static { + Map entryPoints = new HashMap<>(); + Map others = new HashMap<>(); + for (Method m : JNICalls.class.getDeclaredMethods()) { + if (m.getAnnotation(JNICall.class) != null) { + Method existing = entryPoints.put(m.getName(), m); + if (existing != null) { + throw new InternalError("Method annotated by " + JNICall.class.getSimpleName() + + " must have unique name: " + m + " and " + existing); + } + } else { + others.put(m.getName(), m); + } + } + for (Map.Entry e : entryPoints.entrySet()) { + Method existing = others.get(e.getKey()); + if (existing != null) { + throw new InternalError("Method annotated by " + JNICall.class.getSimpleName() + + " must have unique name: " + e.getValue() + " and " + existing); + } + } + JNI_TRANSITION_CLASS = JNICalls.class.getName(); + JNI_TRANSITION_METHODS = Set.copyOf(entryPoints.keySet()); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIExceptionWrapperEntryPoints.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIExceptionWrapperEntryPoints.java new file mode 100644 index 000000000000..2046eaf4f400 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIExceptionWrapperEntryPoints.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.jniutils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Entry points in HotSpot for exception handling from a JNI native method. + */ +final class JNIExceptionWrapperEntryPoints { + + /** + * Updates an exception stack trace by decoding a stack trace from a JNI native method. + * + * @param target the {@link Throwable} to update + * @param serializedStackTrace byte serialized stack trace + * @return the updated {@link Throwable} + */ + @JNIEntryPoint + static Throwable updateStackTrace(Throwable target, byte[] serializedStackTrace) { + try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(serializedStackTrace))) { + int len = in.readInt(); + StackTraceElement[] elements = new StackTraceElement[len]; + for (int i = 0; i < len; i++) { + String className = in.readUTF(); + String methodName = in.readUTF(); + String fileName = in.readUTF(); + int lineNumber = in.readInt(); + elements[i] = new StackTraceElement(className, methodName, fileName.isEmpty() ? null : fileName, lineNumber); + } + target.setStackTrace(elements); + return target; + + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + /** + * Creates an exception used to throw native exception into Java code. + * + * @param message the exception message + * @return exception + */ + @JNIEntryPoint + static Throwable createException(String message) { + return new RuntimeException(message); + } + + @JNIEntryPoint + static byte[] getStackTrace(Throwable throwable) { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(bout)) { + StackTraceElement[] stackTraceElements = throwable.getStackTrace(); + out.writeInt(stackTraceElements.length); + for (StackTraceElement stackTraceElement : stackTraceElements) { + out.writeUTF(stackTraceElement.getClassName()); + out.writeUTF(stackTraceElement.getMethodName()); + String fileName = stackTraceElement.getFileName(); + out.writeUTF(fileName == null ? "" : fileName); + out.writeInt(stackTraceElement.getLineNumber()); + } + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + return bout.toByteArray(); + } + + @JNIEntryPoint + static String getThrowableMessage(Throwable t) { + return t.getMessage(); + } + + @JNIEntryPoint + static String getClassName(Class clz) { + return clz.getName(); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIMethodScope.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIMethodScope.java new file mode 100644 index 000000000000..f222386c4e75 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIMethodScope.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.jniutils; + +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.getFeatureName; + +import java.util.Objects; + +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObject; + +/** + * Scope of a call from HotSpot to native method. This also provides access to the {@link JNIEnv} + * value for the current thread within the native method call. + * + * If the native method call returns a non-primitive value, the return value must be + * {@linkplain #setObjectResult(JObject) set} within the try-with-resources statement and then + * {@linkplain #getObjectResult() retrieved} and returned outside the try-with-resources statement. + * This is necessary to support use of JNI local frames. + */ +public class JNIMethodScope implements AutoCloseable { + + private static final ThreadLocal topScope = new ThreadLocal<>(); + + private final JNIEnv env; + private final JNIMethodScope parent; + private JNIMethodScope leaf; + + /** + * List of scope local {@link HSObject}s that created within this scope. These are + * {@linkplain HSObject#invalidate(HSObject) invalidated} when the scope closes. + */ + HSObject locals; + + /** + * The name for this scope. + */ + private final String scopeName; + + /** + * Gets the {@link JNIEnv} value for the current thread. + */ + public static JNIEnv env() { + return scope().env; + } + + public JNIEnv getEnv() { + return env; + } + + /** + * Gets the inner most {@link JNIMethodScope} value for the current thread. + */ + public static JNIMethodScope scopeOrNull() { + JNIMethodScope scope = topScope.get(); + if (scope == null) { + return null; + } + return scope.leaf; + } + + /** + * Gets the inner most {@link JNIMethodScope} value for the current thread. + */ + public static JNIMethodScope scope() { + JNIMethodScope scope = topScope.get(); + if (scope == null) { + throw new IllegalStateException("Not in the scope of an JNI method call"); + } + return scope.leaf; + } + + /** + * Enters the scope of an native method call. + */ + @SuppressWarnings({"unchecked", "this-escape"}) + public JNIMethodScope(String scopeName, JNIEnv env) { + Objects.requireNonNull(scopeName, "ScopeName must be non null."); + this.scopeName = scopeName; + JNIMethodScope top = topScope.get(); + this.env = env; + if (top == null) { + top = this; + parent = null; + topScope.set(this); + } else { + if (top.env != this.env) { + throw new IllegalStateException("Cannot mix JNI scopes: " + this + " and " + top); + } + parent = top.leaf; + } + top.leaf = this; + JNIUtil.trace(1, "HS->%s[enter]: %s", JNIUtil.getFeatureName(), scopeName); + } + + /** + * Used to copy the handle to an object return value out of the JNI local frame. + */ + private JObject objResult; + + public void setObjectResult(JObject obj) { + objResult = obj; + } + + @SuppressWarnings("unchecked") + public R getObjectResult() { + return (R) objResult; + } + + @Override + public void close() { + JNIUtil.trace(1, "HS->%s[ exit]: %s", getFeatureName(), scopeName); + HSObject.invalidate(locals); + if (parent == null) { + if (topScope.get() != this) { + throw new IllegalStateException("Unexpected JNI scope: " + topScope.get()); + } + topScope.set(null); + } else { + JNIMethodScope top = parent; + while (top.parent != null) { + top = top.parent; + } + top.leaf = parent; + } + } + + public final int depth() { + int depth = 0; + JNIMethodScope ancestor = parent; + while (ancestor != null) { + depth++; + ancestor = ancestor.parent; + } + return depth; + } + + @Override + public String toString() { + return "JNIMethodScope[" + depth() + "]@" + Long.toHexString(env.rawValue()); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIUtil.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIUtil.java new file mode 100644 index 000000000000..88ac118371c1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/JNIUtil.java @@ -0,0 +1,1195 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.jniutils; + +import static com.oracle.svm.jdwp.bridge.jniutils.JNI.JNI_OK; +import static com.oracle.svm.jdwp.bridge.jniutils.JNI.JNI_VERSION_10; +import static org.graalvm.nativeimage.c.type.CTypeConversion.toCString; +import static org.graalvm.word.WordFactory.nullPointer; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JBooleanArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JByteArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JCharArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JClass; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JDoubleArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JFieldID; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JFloatArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JIntArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JLongArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JMethodID; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnvPointer; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObject; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObjectArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JShortArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JString; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JThrowable; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JValue; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JWeak; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JavaVM; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JavaVMAttachArgs; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JavaVMPointer; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.UnmanagedMemory; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CDoublePointer; +import org.graalvm.nativeimage.c.type.CFloatPointer; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.nativeimage.c.type.CLongPointer; +import org.graalvm.nativeimage.c.type.CShortPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.nativeimage.c.type.CTypeConversion.CCharPointerHolder; +import org.graalvm.nativeimage.c.type.VoidPointer; +import org.graalvm.word.WordFactory; + +/** + * Helpers for calling JNI functions. + */ + +public final class JNIUtil { + + private static final String[] METHOD_GET_PLATFORM_CLASS_LOADER = { + "getPlatformClassLoader", + "()Ljava/lang/ClassLoader;" + }; + private static final String[] METHOD_GET_SYSTEM_CLASS_LOADER = { + "getSystemClassLoader", + "()Ljava/lang/ClassLoader;" + }; + private static final String[] METHOD_LOAD_CLASS = { + "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;" + }; + + private static final int ARRAY_COPY_STATIC_BUFFER_SIZE = 8192; + + // Checkstyle: stop + public static boolean IsSameObject(JNIEnv env, JObject ref1, JObject ref2) { + traceJNI("IsSameObject"); + return env.getFunctions().getIsSameObject().call(env, ref1, ref2); + } + + public static void DeleteLocalRef(JNIEnv env, JObject ref) { + traceJNI("DeleteLocalRef"); + env.getFunctions().getDeleteLocalRef().call(env, ref); + } + + public static int PushLocalFrame(JNIEnv env, int capacity) { + traceJNI("PushLocalFrame"); + return env.getFunctions().getPushLocalFrame().call(env, capacity); + } + + public static JObject PopLocalFrame(JNIEnv env, JObject result) { + traceJNI("PopLocalFrame"); + return env.getFunctions().getPopLocalFrame().call(env, result); + } + + public static JClass DefineClass(JNIEnv env, CCharPointer name, JObject loader, CCharPointer buf, int bufLen) { + return env.getFunctions().getDefineClass().call(env, name, loader, buf, bufLen); + } + + public static JClass FindClass(JNIEnv env, CCharPointer name) { + traceJNI("FindClass"); + return env.getFunctions().getFindClass().call(env, name); + } + + public static JClass GetObjectClass(JNIEnv env, JObject object) { + traceJNI("GetObjectClass"); + return env.getFunctions().getGetObjectClass().call(env, object); + } + + public static JMethodID GetStaticMethodID(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig) { + traceJNI("GetStaticMethodID"); + return env.getFunctions().getGetStaticMethodID().call(env, clazz, name, sig); + } + + public static JMethodID GetMethodID(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig) { + traceJNI("GetMethodID"); + return env.getFunctions().getGetMethodID().call(env, clazz, name, sig); + } + + public static JFieldID GetStaticFieldID(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer sig) { + traceJNI("GetStaticFieldID"); + return env.getFunctions().getGetStaticFieldID().call(env, clazz, name, sig); + } + + public static JFieldID GetFieldID(JNIEnv env, JClass clazz, CCharPointer name, CCharPointer signature) { + traceJNI("GetFieldID"); + return env.getFunctions().getGetFieldID().call(env, clazz, name, signature); + } + + public static JObject GetStaticObjectField(JNIEnv env, JClass clazz, JFieldID fieldID) { + traceJNI("GetFieldID"); + return env.getFunctions().getGetStaticObjectField().call(env, clazz, fieldID); + } + + public static int GetIntField(JNIEnv env, JObject object, JFieldID fieldID) { + traceJNI("GetIntField"); + return env.getFunctions().getGetIntField().call(env, object, fieldID); + } + + public static JObjectArray NewObjectArray(JNIEnv env, int len, JClass componentClass, JObject initialElement) { + traceJNI("NewObjectArray"); + return env.getFunctions().getNewObjectArray().call(env, len, componentClass, initialElement); + } + + public static JBooleanArray NewBooleanArray(JNIEnv env, int len) { + traceJNI("NewBooleanArray"); + return env.getFunctions().getNewBooleanArray().call(env, len); + } + + public static JByteArray NewByteArray(JNIEnv env, int len) { + traceJNI("NewByteArray"); + return env.getFunctions().getNewByteArray().call(env, len); + } + + public static JCharArray NewCharArray(JNIEnv env, int len) { + traceJNI("NewCharArray"); + return env.getFunctions().getNewCharArray().call(env, len); + } + + public static JShortArray NewShortArray(JNIEnv env, int len) { + traceJNI("NewShortArray"); + return env.getFunctions().getNewShortArray().call(env, len); + } + + public static JIntArray NewIntArray(JNIEnv env, int len) { + traceJNI("NewIntArray"); + return env.getFunctions().getNewIntArray().call(env, len); + } + + public static JLongArray NewLongArray(JNIEnv env, int len) { + traceJNI("NewLongArray"); + return env.getFunctions().getNewLongArray().call(env, len); + } + + public static JFloatArray NewFloatArray(JNIEnv env, int len) { + traceJNI("NewFloatArray"); + return env.getFunctions().getNewFloatArray().call(env, len); + } + + public static JDoubleArray NewDoubleArray(JNIEnv env, int len) { + traceJNI("NewDoubleArray"); + return env.getFunctions().getNewDoubleArray().call(env, len); + } + + public static int GetArrayLength(JNIEnv env, JArray array) { + traceJNI("GetArrayLength"); + return env.getFunctions().getGetArrayLength().call(env, array); + } + + public static void SetObjectArrayElement(JNIEnv env, JObjectArray array, int index, JObject value) { + traceJNI("SetObjectArrayElement"); + env.getFunctions().getSetObjectArrayElement().call(env, array, index, value); + } + + public static JObject GetObjectArrayElement(JNIEnv env, JObjectArray array, int index) { + traceJNI("GetObjectArrayElement"); + return env.getFunctions().getGetObjectArrayElement().call(env, array, index); + } + + public static CCharPointer GetBooleanArrayElements(JNIEnv env, JBooleanArray array, JValue isCopy) { + traceJNI("GetBooleanArrayElements"); + return env.getFunctions().getGetBooleanArrayElements().call(env, array, isCopy); + } + + public static CCharPointer GetByteArrayElements(JNIEnv env, JByteArray array, JValue isCopy) { + traceJNI("GetByteArrayElements"); + return env.getFunctions().getGetByteArrayElements().call(env, array, isCopy); + } + + public static CShortPointer GetCharArrayElements(JNIEnv env, JCharArray array, JValue isCopy) { + traceJNI("GetCharArrayElements"); + return env.getFunctions().getGetCharArrayElements().call(env, array, isCopy); + } + + public static CShortPointer GetShortArrayElements(JNIEnv env, JShortArray array, JValue isCopy) { + traceJNI("GetShortArrayElements"); + return env.getFunctions().getGetShortArrayElements().call(env, array, isCopy); + } + + public static CIntPointer GetIntArrayElements(JNIEnv env, JIntArray array, JValue isCopy) { + traceJNI("GetIntArrayElements"); + return env.getFunctions().getGetIntArrayElements().call(env, array, isCopy); + } + + public static CLongPointer GetLongArrayElements(JNIEnv env, JLongArray array, JValue isCopy) { + traceJNI("GetLongArrayElements"); + return env.getFunctions().getGetLongArrayElements().call(env, array, isCopy); + } + + public static CFloatPointer GetFloatArrayElements(JNIEnv env, JFloatArray array, JValue isCopy) { + traceJNI("GetFloatArrayElements"); + return env.getFunctions().getGetFloatArrayElements().call(env, array, isCopy); + } + + public static CDoublePointer GetDoubleArrayElements(JNIEnv env, JDoubleArray array, JValue isCopy) { + traceJNI("GetFloatArrayElements"); + return env.getFunctions().getGetDoubleArrayElements().call(env, array, isCopy); + } + + public static void ReleaseBooleanArrayElements(JNIEnv env, JBooleanArray array, CCharPointer elems, int mode) { + traceJNI("ReleaseBooleanArrayElements"); + env.getFunctions().getReleaseBooleanArrayElements().call(env, array, elems, mode); + } + + public static void ReleaseByteArrayElements(JNIEnv env, JByteArray array, CCharPointer elems, int mode) { + traceJNI("ReleaseByteArrayElements"); + env.getFunctions().getReleaseByteArrayElements().call(env, array, elems, mode); + } + + public static void ReleaseCharArrayElements(JNIEnv env, JCharArray array, CShortPointer elems, int mode) { + traceJNI("ReleaseCharArrayElements"); + env.getFunctions().getReleaseCharArrayElements().call(env, array, elems, mode); + } + + public static void ReleaseShortArrayElements(JNIEnv env, JShortArray array, CShortPointer elems, int mode) { + traceJNI("ReleaseShortArrayElements"); + env.getFunctions().getReleaseShortArrayElements().call(env, array, elems, mode); + } + + public static void ReleaseIntArrayElements(JNIEnv env, JIntArray array, CIntPointer elems, int mode) { + traceJNI("ReleaseIntArrayElements"); + env.getFunctions().getReleaseIntArrayElements().call(env, array, elems, mode); + } + + public static void ReleaseLongArrayElements(JNIEnv env, JLongArray array, CLongPointer elems, int mode) { + traceJNI("ReleaseLongArrayElements"); + env.getFunctions().getReleaseLongArrayElements().call(env, array, elems, mode); + } + + public static void ReleaseFloatArrayElements(JNIEnv env, JFloatArray array, CFloatPointer elems, int mode) { + traceJNI("ReleaseFloatArrayElements"); + env.getFunctions().getReleaseFloatArrayElements().call(env, array, elems, mode); + } + + public static void ReleaseDoubleArrayElements(JNIEnv env, JDoubleArray array, CDoublePointer elems, int mode) { + traceJNI("ReleaseDoubleArrayElements"); + env.getFunctions().getReleaseDoubleArrayElements().call(env, array, elems, mode); + } + + public static void GetBooleanArrayRegion(JNIEnv env, JBooleanArray array, int offset, int len, CCharPointer buff) { + traceJNI("GetBooleanArrayRegion"); + env.getFunctions().getGetBooleanArrayRegion().call(env, array, offset, len, buff); + } + + public static void GetByteArrayRegion(JNIEnv env, JByteArray array, int offset, int len, CCharPointer buff) { + traceJNI("GetByteArrayRegion"); + env.getFunctions().getGetByteArrayRegion().call(env, array, offset, len, buff); + } + + public static void GetCharArrayRegion(JNIEnv env, JCharArray array, int offset, int len, CShortPointer buff) { + traceJNI("GetCharArrayRegion"); + env.getFunctions().getGetCharArrayRegion().call(env, array, offset, len, buff); + } + + public static void GetShortArrayRegion(JNIEnv env, JShortArray array, int offset, int len, CShortPointer buff) { + traceJNI("GetShortArrayRegion"); + env.getFunctions().getGetShortArrayRegion().call(env, array, offset, len, buff); + } + + public static void GetIntArrayRegion(JNIEnv env, JIntArray array, int offset, int len, CIntPointer buff) { + traceJNI("GetIntArrayRegion"); + env.getFunctions().getGetIntArrayRegion().call(env, array, offset, len, buff); + } + + public static void GetLongArrayRegion(JNIEnv env, JLongArray array, int offset, int len, CLongPointer buff) { + traceJNI("GetLongArrayRegion"); + env.getFunctions().getGetLongArrayRegion().call(env, array, offset, len, buff); + } + + public static void GetFloatArrayRegion(JNIEnv env, JFloatArray array, int offset, int len, CFloatPointer buff) { + traceJNI("GetFloatArrayRegion"); + env.getFunctions().getGetFloatArrayRegion().call(env, array, offset, len, buff); + } + + public static void GetDoubleArrayRegion(JNIEnv env, JDoubleArray array, int offset, int len, CDoublePointer buff) { + traceJNI("GetDoubleArrayRegion"); + env.getFunctions().getGetDoubleArrayRegion().call(env, array, offset, len, buff); + } + + public static void SetBooleanArrayRegion(JNIEnv env, JBooleanArray array, int offset, int len, CCharPointer buff) { + traceJNI("SetBooleanArrayRegion"); + env.getFunctions().getSetBooleanArrayRegion().call(env, array, offset, len, buff); + } + + public static void SetByteArrayRegion(JNIEnv env, JByteArray array, int offset, int len, CCharPointer buff) { + traceJNI("SetByteArrayRegion"); + env.getFunctions().getSetByteArrayRegion().call(env, array, offset, len, buff); + } + + public static void SetCharArrayRegion(JNIEnv env, JCharArray array, int offset, int len, CShortPointer buff) { + traceJNI("SetCharArrayRegion"); + env.getFunctions().getSetCharArrayRegion().call(env, array, offset, len, buff); + } + + public static void SetShortArrayRegion(JNIEnv env, JShortArray array, int offset, int len, CShortPointer buff) { + traceJNI("SetShortArrayRegion"); + env.getFunctions().getSetShortArrayRegion().call(env, array, offset, len, buff); + } + + public static void SetIntArrayRegion(JNIEnv env, JIntArray array, int offset, int len, CIntPointer buff) { + traceJNI("SetIntArrayRegion"); + env.getFunctions().getSetIntArrayRegion().call(env, array, offset, len, buff); + } + + public static void SetLongArrayRegion(JNIEnv env, JLongArray array, int offset, int len, CLongPointer buff) { + traceJNI("SetLongArrayRegion"); + env.getFunctions().getSetLongArrayRegion().call(env, array, offset, len, buff); + } + + public static void SetFloatArrayRegion(JNIEnv env, JFloatArray array, int offset, int len, CFloatPointer buff) { + traceJNI("SetFloatArrayRegion"); + env.getFunctions().getSetFloatArrayRegion().call(env, array, offset, len, buff); + } + + public static void SetDoubleArrayRegion(JNIEnv env, JDoubleArray array, int offset, int len, CDoublePointer buff) { + traceJNI("SetDoubleArrayRegion"); + env.getFunctions().getSetDoubleArrayRegion().call(env, array, offset, len, buff); + } + + public static JavaVM GetJavaVM(JNIEnv env) { + traceJNI("GetJavaVM"); + JavaVMPointer javaVMPointer = StackValue.get(JavaVMPointer.class); + if (env.getFunctions().getGetJavaVM().call(env, javaVMPointer) == JNI_OK) { + return javaVMPointer.readJavaVM(); + } else { + return WordFactory.nullPointer(); + } + } + + public static JNIEnv GetEnv(JavaVM vm) { + traceJNI("GetEnv"); + JNIEnvPointer envPointer = StackValue.get(JNIEnvPointer.class); + if (vm.getFunctions().getGetEnv().call(vm, envPointer, JNI_VERSION_10) == JNI_OK) { + return envPointer.readJNIEnv(); + } else { + return WordFactory.nullPointer(); + } + } + + public static JNIEnv AttachCurrentThread(JavaVM vm, JavaVMAttachArgs args) { + traceJNI("AttachCurrentThread"); + JNIEnvPointer envPointer = StackValue.get(JNIEnvPointer.class); + if (vm.getFunctions().getAttachCurrentThread().call(vm, envPointer, args) == JNI_OK) { + return envPointer.readJNIEnv(); + } else { + return WordFactory.nullPointer(); + } + } + + public static JNIEnv AttachCurrentThreadAsDaemon(JavaVM vm, JavaVMAttachArgs args) { + traceJNI("AttachCurrentThreadAsDaemon"); + JNIEnvPointer envPointer = StackValue.get(JNIEnvPointer.class); + if (vm.getFunctions().getAttachCurrentThreadAsDaemon().call(vm, envPointer, args) == JNI_OK) { + return envPointer.readJNIEnv(); + } else { + return WordFactory.nullPointer(); + } + } + + public static boolean DetachCurrentThread(JavaVM vm) { + traceJNI("DetachCurrentThread"); + return vm.getFunctions().getDetachCurrentThread().call(vm) == JNI_OK; + } + + public static void Throw(JNIEnv env, JThrowable throwable) { + traceJNI("Throw"); + env.getFunctions().getThrow().call(env, throwable); + } + + public static boolean ExceptionCheck(JNIEnv env) { + traceJNI("ExceptionCheck"); + return env.getFunctions().getExceptionCheck().call(env); + } + + public static void ExceptionClear(JNIEnv env) { + traceJNI("ExceptionClear"); + env.getFunctions().getExceptionClear().call(env); + } + + public static void ExceptionDescribe(JNIEnv env) { + traceJNI("ExceptionDescribe"); + env.getFunctions().getExceptionDescribe().call(env); + } + + public static JThrowable ExceptionOccurred(JNIEnv env) { + traceJNI("ExceptionOccurred"); + return env.getFunctions().getExceptionOccurred().call(env); + } + + /** + * Creates a new global reference. + * + * @param env the JNIEnv + * @param ref JObject to create JNI global reference for + * @param type type of the object, used only for tracing to distinguish global references + * @return JNI global reference for given {@link JObject} + */ + @SuppressWarnings("unchecked") + public static T NewGlobalRef(JNIEnv env, T ref, String type) { + traceJNI("NewGlobalRef"); + T res = (T) env.getFunctions().getNewGlobalRef().call(env, ref); + if (tracingAt(3)) { + trace(3, "New global reference for 0x%x of type %s -> 0x%x", ref.rawValue(), type, res.rawValue()); + } + return res; + } + + public static void DeleteGlobalRef(JNIEnv env, JObject ref) { + traceJNI("DeleteGlobalRef"); + if (tracingAt(3)) { + trace(3, "Delete global reference 0x%x", ref.rawValue()); + } + env.getFunctions().getDeleteGlobalRef().call(env, ref); + } + + /** + * Creates a new weak global reference. + * + * @param env the JNIEnv + * @param ref JObject to create JNI weak global reference for + * @param type type of the object, used only for tracing to distinguish global references + * @return JNI weak global reference for given {@link JObject} + */ + public static JWeak NewWeakGlobalRef(JNIEnv env, JObject ref, String type) { + traceJNI("NewWeakGlobalRef"); + JWeak res = env.getFunctions().getNewWeakGlobalRef().call(env, ref); + if (tracingAt(3)) { + trace(3, "New weak global reference for 0x%x of type %s -> 0x%x", ref.rawValue(), type, res.rawValue()); + } + return res; + } + + public static JObject NewLocalRef(JNIEnv env, JObject ref) { + traceJNI("NewLocalRef"); + return env.getFunctions().getNewLocalRef().call(env, ref); + } + + public static void DeleteWeakGlobalRef(JNIEnv env, JWeak ref) { + traceJNI("DeleteWeakGlobalRef"); + if (tracingAt(3)) { + trace(3, "Delete weak global reference 0x%x", ref.rawValue()); + } + env.getFunctions().getDeleteWeakGlobalRef().call(env, ref); + } + + public static VoidPointer GetDirectBufferAddress(JNIEnv env, JObject buf) { + traceJNI("GetDirectBufferAddress"); + return env.getFunctions().getGetDirectBufferAddress().call(env, buf); + } + + public static boolean IsInstanceOf(JNIEnv env, JObject obj, JClass clazz) { + traceJNI("IsInstanceOf"); + return env.getFunctions().getIsInstanceOf().call(env, obj, clazz); + } + + // Checkstyle: resume + + private static void traceJNI(String function) { + trace(2, "%s->JNI: %s", getFeatureName(), function); + } + + private JNIUtil() { + } + + /** + * Decodes a string in the HotSpot heap to a local {@link String}. + */ + public static String createString(JNIEnv env, JString hsString) { + if (hsString.isNull()) { + return null; + } + int len = env.getFunctions().getGetStringLength().call(env, hsString); + CShortPointer unicode = env.getFunctions().getGetStringChars().call(env, hsString, WordFactory.nullPointer()); + try { + char[] data = new char[len]; + for (int i = 0; i < len; i++) { + data[i] = (char) unicode.read(i); + } + return new String(data); + } finally { + env.getFunctions().getReleaseStringChars().call(env, hsString, unicode); + } + } + + /** + * Creates a String in the HotSpot heap from {@code string}. + */ + public static JString createHSString(JNIEnv env, String string) { + if (string == null) { + return WordFactory.nullPointer(); + } + int len = string.length(); + CShortPointer buffer = UnmanagedMemory.malloc(len << 1); + try { + for (int i = 0; i < len; i++) { + buffer.write(i, (short) string.charAt(i)); + } + return env.getFunctions().getNewString().call(env, buffer, len); + } finally { + UnmanagedMemory.free(buffer); + } + } + + public static boolean[] createArray(JNIEnv env, JBooleanArray booleanArray) { + if (booleanArray.isNull()) { + return null; + } + int len = GetArrayLength(env, booleanArray); + boolean[] booleans = new boolean[len]; + arrayCopy(env, booleanArray, 0, booleans, 0, len); + return booleans; + } + + public static JBooleanArray createHSArray(JNIEnv jniEnv, boolean[] a) { + if (a == null) { + return WordFactory.nullPointer(); + } + JBooleanArray array = NewBooleanArray(jniEnv, a.length); + arrayCopy(jniEnv, a, 0, array, 0, a.length); + return array; + } + + public static byte[] createArray(JNIEnv env, JByteArray byteArray) { + if (byteArray.isNull()) { + return null; + } + int len = GetArrayLength(env, byteArray); + byte[] bytes = new byte[len]; + arrayCopy(env, byteArray, 0, bytes, 0, len); + return bytes; + } + + public static JByteArray createHSArray(JNIEnv jniEnv, byte[] a) { + if (a == null) { + return WordFactory.nullPointer(); + } + JByteArray array = NewByteArray(jniEnv, a.length); + arrayCopy(jniEnv, a, 0, array, 0, a.length); + return array; + } + + public static char[] createArray(JNIEnv env, JCharArray charArray) { + if (charArray.isNull()) { + return null; + } + int len = GetArrayLength(env, charArray); + char[] chars = new char[len]; + arrayCopy(env, charArray, 0, chars, 0, len); + return chars; + } + + public static JCharArray createHSArray(JNIEnv jniEnv, char[] a) { + if (a == null) { + return WordFactory.nullPointer(); + } + JCharArray array = NewCharArray(jniEnv, a.length); + arrayCopy(jniEnv, a, 0, array, 0, a.length); + return array; + } + + public static short[] createArray(JNIEnv env, JShortArray shortArray) { + if (shortArray.isNull()) { + return null; + } + int len = GetArrayLength(env, shortArray); + short[] shorts = new short[len]; + arrayCopy(env, shortArray, 0, shorts, 0, len); + return shorts; + } + + public static JShortArray createHSArray(JNIEnv jniEnv, short[] a) { + if (a == null) { + return WordFactory.nullPointer(); + } + JShortArray array = NewShortArray(jniEnv, a.length); + arrayCopy(jniEnv, a, 0, array, 0, a.length); + return array; + } + + public static int[] createArray(JNIEnv env, JIntArray intArray) { + if (intArray.isNull()) { + return null; + } + int len = GetArrayLength(env, intArray); + int[] ints = new int[len]; + arrayCopy(env, intArray, 0, ints, 0, len); + return ints; + } + + public static JIntArray createHSArray(JNIEnv jniEnv, int[] a) { + if (a == null) { + return WordFactory.nullPointer(); + } + JIntArray array = NewIntArray(jniEnv, a.length); + arrayCopy(jniEnv, a, 0, array, 0, a.length); + return array; + } + + public static long[] createArray(JNIEnv env, JLongArray longArray) { + if (longArray.isNull()) { + return null; + } + int len = GetArrayLength(env, longArray); + long[] longs = new long[len]; + arrayCopy(env, longArray, 0, longs, 0, len); + return longs; + } + + public static JLongArray createHSArray(JNIEnv jniEnv, long[] a) { + if (a == null) { + return WordFactory.nullPointer(); + } + JLongArray array = NewLongArray(jniEnv, a.length); + arrayCopy(jniEnv, a, 0, array, 0, a.length); + return array; + } + + public static float[] createArray(JNIEnv env, JFloatArray floatArray) { + if (floatArray.isNull()) { + return null; + } + int len = GetArrayLength(env, floatArray); + float[] floats = new float[len]; + arrayCopy(env, floatArray, 0, floats, 0, len); + return floats; + } + + public static JFloatArray createHSArray(JNIEnv jniEnv, float[] a) { + if (a == null) { + return WordFactory.nullPointer(); + } + JFloatArray array = NewFloatArray(jniEnv, a.length); + arrayCopy(jniEnv, a, 0, array, 0, a.length); + return array; + } + + public static double[] createArray(JNIEnv env, JDoubleArray doubleArray) { + if (doubleArray.isNull()) { + return null; + } + int len = GetArrayLength(env, doubleArray); + double[] doubles = new double[len]; + arrayCopy(env, doubleArray, 0, doubles, 0, len); + return doubles; + } + + public static JDoubleArray createHSArray(JNIEnv jniEnv, double[] a) { + if (a == null) { + return WordFactory.nullPointer(); + } + JDoubleArray array = NewDoubleArray(jniEnv, a.length); + arrayCopy(jniEnv, a, 0, array, 0, a.length); + return array; + } + + public static JObjectArray createHSArray(JNIEnv jniEnv, Object[] array, int sourcePosition, int length, String componentTypeBinaryName) { + JObjectArray hsArray; + if (array != null) { + hsArray = JNIUtil.NewObjectArray(jniEnv, length, JNIUtil.findClass(jniEnv, WordFactory.nullPointer(), componentTypeBinaryName, true), WordFactory.nullPointer()); + for (int i = 0; i < length; i++) { + HSObject element = (HSObject) array[sourcePosition + i]; + JObject hsElement = element != null ? element.getHandle() : WordFactory.nullPointer(); + JNIUtil.SetObjectArrayElement(jniEnv, hsArray, i, hsElement); + } + } else { + hsArray = WordFactory.nullPointer(); + } + return hsArray; + } + + public static void arrayCopy(JNIEnv jniEnv, JBooleanArray src, int srcPos, boolean[] dest, int destPos, int length) { + CCharPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + CCharPointer booleanPointer = length <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(length); + try { + GetBooleanArrayRegion(jniEnv, src, srcPos, length, booleanPointer); + for (int i = 0; i < length; i++) { + dest[destPos + i] = booleanPointer.addressOf(i).read() != 0; + } + } finally { + if (booleanPointer != staticBuffer) { + UnmanagedMemory.free(booleanPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, boolean[] src, int srcPos, JBooleanArray dest, int destPos, int length) { + CCharPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + CCharPointer booleanPointer = length <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(length); + try { + for (int i = 0; i < length; i++) { + booleanPointer.write(i, src[srcPos + i] ? (byte) 1 : 0); + } + SetBooleanArrayRegion(jniEnv, dest, destPos, length, booleanPointer); + } finally { + if (booleanPointer != staticBuffer) { + UnmanagedMemory.free(booleanPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, JByteArray src, int srcPos, byte[] dest, int destPos, int length) { + CCharPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + CCharPointer bytePointer = length <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(length); + try { + GetByteArrayRegion(jniEnv, src, srcPos, length, bytePointer); + CTypeConversion.asByteBuffer(bytePointer, length).get(dest, destPos, length); + } finally { + if (bytePointer != staticBuffer) { + UnmanagedMemory.free(bytePointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, byte[] src, int srcPos, JByteArray dest, int destPos, int length) { + CCharPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + CCharPointer bytePointer = length <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(length); + try { + CTypeConversion.asByteBuffer(bytePointer, length).put(src, srcPos, length); + SetByteArrayRegion(jniEnv, dest, destPos, length, bytePointer); + } finally { + if (bytePointer != staticBuffer) { + UnmanagedMemory.free(bytePointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, JCharArray src, int srcPos, char[] dest, int destPos, int length) { + CShortPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Character.BYTES; + CShortPointer shortPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + GetCharArrayRegion(jniEnv, src, srcPos, length, shortPointer); + CTypeConversion.asByteBuffer(shortPointer, bytesLength).asCharBuffer().get(dest, destPos, length); + } finally { + if (shortPointer != staticBuffer) { + UnmanagedMemory.free(shortPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, char[] src, int srcPos, JCharArray dest, int destPos, int length) { + CShortPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Character.BYTES; + CShortPointer shortPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + CTypeConversion.asByteBuffer(shortPointer, bytesLength).asCharBuffer().put(src, srcPos, length); + SetCharArrayRegion(jniEnv, dest, destPos, length, shortPointer); + } finally { + if (shortPointer != staticBuffer) { + UnmanagedMemory.free(shortPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, JShortArray src, int srcPos, short[] dest, int destPos, int length) { + CShortPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Short.BYTES; + CShortPointer shortPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + GetShortArrayRegion(jniEnv, src, srcPos, length, shortPointer); + CTypeConversion.asByteBuffer(shortPointer, bytesLength).asShortBuffer().get(dest, destPos, length); + } finally { + if (shortPointer != staticBuffer) { + UnmanagedMemory.free(shortPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, short[] src, int srcPos, JShortArray dest, int destPos, int length) { + CShortPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Short.BYTES; + CShortPointer shortPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + CTypeConversion.asByteBuffer(shortPointer, bytesLength).asShortBuffer().put(src, srcPos, length); + SetShortArrayRegion(jniEnv, dest, destPos, length, shortPointer); + } finally { + if (shortPointer != staticBuffer) { + UnmanagedMemory.free(shortPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, JIntArray src, int srcPos, int[] dest, int destPos, int length) { + CIntPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Integer.BYTES; + CIntPointer intPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + GetIntArrayRegion(jniEnv, src, srcPos, length, intPointer); + CTypeConversion.asByteBuffer(intPointer, bytesLength).asIntBuffer().get(dest, destPos, length); + } finally { + if (intPointer != staticBuffer) { + UnmanagedMemory.free(intPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, int[] src, int srcPos, JIntArray dest, int destPos, int length) { + CIntPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Integer.BYTES; + CIntPointer intPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + CTypeConversion.asByteBuffer(intPointer, bytesLength).asIntBuffer().put(src, srcPos, length); + SetIntArrayRegion(jniEnv, dest, destPos, length, intPointer); + } finally { + if (intPointer != staticBuffer) { + UnmanagedMemory.free(intPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, JLongArray src, int srcPos, long[] dest, int destPos, int length) { + CLongPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Long.BYTES; + CLongPointer longPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + GetLongArrayRegion(jniEnv, src, srcPos, length, longPointer); + CTypeConversion.asByteBuffer(longPointer, bytesLength).asLongBuffer().get(dest, destPos, length); + } finally { + if (longPointer != staticBuffer) { + UnmanagedMemory.free(longPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, long[] src, int srcPos, JLongArray dest, int destPos, int length) { + CLongPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Long.BYTES; + CLongPointer longPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + CTypeConversion.asByteBuffer(longPointer, bytesLength).asLongBuffer().put(src, srcPos, length); + SetLongArrayRegion(jniEnv, dest, destPos, length, longPointer); + } finally { + if (longPointer != staticBuffer) { + UnmanagedMemory.free(longPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, JFloatArray src, int srcPos, float[] dest, int destPos, int length) { + CFloatPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Float.BYTES; + CFloatPointer floatPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + GetFloatArrayRegion(jniEnv, src, srcPos, length, floatPointer); + CTypeConversion.asByteBuffer(floatPointer, bytesLength).asFloatBuffer().get(dest, destPos, length); + } finally { + if (floatPointer != staticBuffer) { + UnmanagedMemory.free(floatPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, float[] src, int srcPos, JFloatArray dest, int destPos, int length) { + CFloatPointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Float.BYTES; + CFloatPointer floatPointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + CTypeConversion.asByteBuffer(floatPointer, bytesLength).asFloatBuffer().put(src, srcPos, length); + SetFloatArrayRegion(jniEnv, dest, destPos, length, floatPointer); + } finally { + if (floatPointer != staticBuffer) { + UnmanagedMemory.free(floatPointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, JDoubleArray src, int srcPos, double[] dest, int destPos, int length) { + CDoublePointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Double.BYTES; + CDoublePointer doublePointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + GetDoubleArrayRegion(jniEnv, src, srcPos, length, doublePointer); + CTypeConversion.asByteBuffer(doublePointer, bytesLength).asDoubleBuffer().get(dest, destPos, length); + } finally { + if (doublePointer != staticBuffer) { + UnmanagedMemory.free(doublePointer); + } + } + } + + public static void arrayCopy(JNIEnv jniEnv, double[] src, int srcPos, JDoubleArray dest, int destPos, int length) { + CDoublePointer staticBuffer = StackValue.get(ARRAY_COPY_STATIC_BUFFER_SIZE); + int bytesLength = length * Double.BYTES; + CDoublePointer doublePointer = bytesLength <= ARRAY_COPY_STATIC_BUFFER_SIZE ? staticBuffer : UnmanagedMemory.malloc(bytesLength); + try { + CTypeConversion.asByteBuffer(doublePointer, bytesLength).asDoubleBuffer().put(src, srcPos, length); + SetDoubleArrayRegion(jniEnv, dest, destPos, length, doublePointer); + } finally { + if (doublePointer != staticBuffer) { + UnmanagedMemory.free(doublePointer); + } + } + } + + /** + * Converts a fully qualified Java class name from Java source format (e.g. + * {@code "java.lang.getString"}) to internal format (e.g. {@code "Ljava/lang/getString;"}. + */ + public static String getInternalName(String fqn) { + return "L" + getBinaryName(fqn) + ";"; + } + + /** + * Converts a fully qualified Java class name from Java source format (e.g. + * {@code "java.lang.getString"}) to binary format (e.g. {@code "java/lang/getString"}. + */ + public static String getBinaryName(String fqn) { + return fqn.replace('.', '/'); + } + + /** + * Creates a JVM method signature as specified in the Sections 4.3.3 of the JVM Specification. + */ + public static String encodeMethodSignature(Class returnType, Class... parameterTypes) { + StringBuilder builder = new StringBuilder("("); + for (Class type : parameterTypes) { + encodeType(type, builder); + } + builder.append(")"); + encodeType(returnType, builder); + return builder.toString(); + } + + /** + * Creates a JVM field signature as specified in the Sections 4.3.2 of the JVM Specification. + */ + public static String encodeFieldSignature(Class type) { + StringBuilder res = new StringBuilder(); + encodeType(type, res); + return res.toString(); + } + + private static void encodeType(Class type, StringBuilder buf) { + String desc; + if (type == boolean.class) { + desc = "Z"; + } else if (type == byte.class) { + desc = "B"; + } else if (type == char.class) { + desc = "C"; + } else if (type == short.class) { + desc = "S"; + } else if (type == int.class) { + desc = "I"; + } else if (type == long.class) { + desc = "J"; + } else if (type == float.class) { + desc = "F"; + } else if (type == double.class) { + desc = "D"; + } else if (type == void.class) { + desc = "V"; + } else if (type.isArray()) { + buf.append('['); + encodeType(type.getComponentType(), buf); + return; + } else { + desc = "L" + type.getName().replace('.', '/') + ";"; + } + buf.append(desc); + } + + /** + * Returns a {@link JClass} for given binary name. + */ + public static JClass findClass(JNIEnv env, String binaryName) { + trace(1, "%s->HS: findClass %s", getFeatureName(), binaryName); + try (CCharPointerHolder name = CTypeConversion.toCString(binaryName)) { + return JNIUtil.FindClass(env, name.get()); + } + } + + /** + * Finds a class in HotSpot heap using a given {@code ClassLoader}. + * + * @param env the {@code JNIEnv} + * @param binaryName the class binary name + */ + public static JClass findClass(JNIEnv env, JObject classLoader, String binaryName) { + if (classLoader.isNull()) { + throw new IllegalArgumentException("ClassLoader must be non null."); + } + trace(1, "%s->HS: findClass %s", getFeatureName(), binaryName); + JMethodID findClassId = findMethod(env, JNIUtil.GetObjectClass(env, classLoader), false, false, METHOD_LOAD_CLASS[0], METHOD_LOAD_CLASS[1]); + JValue params = StackValue.get(1, JValue.class); + params.addressOf(0).setJObject(JNIUtil.createHSString(env, binaryName.replace('/', '.'))); + return (JClass) env.getFunctions().getCallObjectMethodA().call(env, classLoader, findClassId, params); + } + + /** + * Finds a class in HotSpot heap using JNI. + * + * @param env the {@code JNIEnv} + * @param classLoader the class loader to find class in or {@link WordFactory#nullPointer() NULL + * pointer}. + * @param binaryName the class binary name + * @param required if {@code true} the {@link JNIExceptionWrapper} is thrown when the class is + * not found. If {@code false} the {@code NULL pointer} is returned when the class is + * not found. + */ + public static JClass findClass(JNIEnv env, JObject classLoader, String binaryName, boolean required) { + Class allowedException = null; + try { + if (classLoader.isNonNull()) { + allowedException = required ? null : ClassNotFoundException.class; + return findClass(env, classLoader, binaryName); + } else { + allowedException = required ? null : NoClassDefFoundError.class; + return findClass(env, binaryName); + } + } finally { + JNIExceptionWrapper.wrapAndThrowPendingJNIException(env, + allowedException == null ? JNIExceptionWrapper.ExceptionHandler.DEFAULT : JNIExceptionWrapper.ExceptionHandler.allowExceptions(allowedException)); + } + } + + /** + * Returns a ClassLoader used to load the compiler classes. + */ + public static JObject getJVMCIClassLoader(JNIEnv env) { + JClass clazz; + try (CCharPointerHolder className = CTypeConversion.toCString(JNIUtil.getBinaryName(ClassLoader.class.getName()))) { + clazz = JNIUtil.FindClass(env, className.get()); + } + if (clazz.isNull()) { + throw new InternalError("No such class " + ClassLoader.class.getName()); + } + JMethodID getClassLoaderId = findMethod(env, clazz, true, true, METHOD_GET_PLATFORM_CLASS_LOADER[0], METHOD_GET_PLATFORM_CLASS_LOADER[1]); + if (getClassLoaderId.isNull()) { + throw new InternalError(String.format("Cannot find method %s in class %s.", METHOD_GET_PLATFORM_CLASS_LOADER[0], ClassLoader.class.getName())); + } + return env.getFunctions().getCallStaticObjectMethodA().call(env, clazz, getClassLoaderId, nullPointer()); + } + + public static JObject getClassLoader(JNIEnv env, JClass clazz) { + if (clazz.isNull()) { + throw new NullPointerException(); + } + // Class + JClass classClass = GetObjectClass(env, clazz); // Class + JMethodID getClassLoader = JNIUtil.findMethod(env, classClass, false, "getClassLoader", "()Ljava/lang/ClassLoader;"); + if (getClassLoader.isNull()) { + throw new NullPointerException("Not found: getClassLoader()"); + } + return env.getFunctions().getCallObjectMethodA().call(env, clazz, getClassLoader, nullPointer()); + } + + /** + * Returns the {@link ClassLoader#getSystemClassLoader()}. + */ + public static JObject getSystemClassLoader(JNIEnv env) { + JClass clazz; + try (CCharPointerHolder className = CTypeConversion.toCString(JNIUtil.getBinaryName(ClassLoader.class.getName()))) { + clazz = JNIUtil.FindClass(env, className.get()); + } + if (clazz.isNull()) { + throw new InternalError("No such class " + ClassLoader.class.getName()); + } + JMethodID getClassLoaderId = findMethod(env, clazz, true, true, METHOD_GET_SYSTEM_CLASS_LOADER[0], METHOD_GET_SYSTEM_CLASS_LOADER[1]); + if (getClassLoaderId.isNull()) { + throw new InternalError(String.format("Cannot find method %s in class %s.", METHOD_GET_SYSTEM_CLASS_LOADER[0], ClassLoader.class.getName())); + } + return env.getFunctions().getCallStaticObjectMethodA().call(env, clazz, getClassLoaderId, nullPointer()); + } + + public static JMethodID findMethod(JNIEnv env, JClass clazz, boolean staticMethod, String methodName, String methodSignature) { + return findMethod(env, clazz, staticMethod, false, methodName, methodSignature); + } + + static JMethodID findMethod(JNIEnv env, JClass clazz, boolean staticMethod, boolean required, + String methodName, String methodSignature) { + JMethodID result; + try (CCharPointerHolder name = toCString(methodName); CCharPointerHolder sig = toCString(methodSignature)) { + result = staticMethod ? GetStaticMethodID(env, clazz, name.get(), sig.get()) : GetMethodID(env, clazz, name.get(), sig.get()); + JNIExceptionWrapper.wrapAndThrowPendingJNIException(env, + required ? JNIExceptionWrapper.ExceptionHandler.DEFAULT : JNIExceptionWrapper.ExceptionHandler.allowExceptions(NoSuchMethodError.class)); + return result; + } + } + + public static JFieldID findField(JNIEnv env, JClass clazz, boolean staticField, String fieldName, String fieldSignature) { + JFieldID result; + try (CCharPointerHolder name = toCString(fieldName); CCharPointerHolder sig = toCString(fieldSignature)) { + result = staticField ? GetStaticFieldID(env, clazz, name.get(), sig.get()) : GetFieldID(env, clazz, name.get(), sig.get()); + JNIExceptionWrapper.wrapAndThrowPendingJNIException(env); + return result; + } + } + + /** + * Attaches the current C thread to a Java Thread. + * + * @param vm the {@link JavaVM} pointer. + * @param daemon if true attaches the thread as a daemon thread. + * @param name the name of the Java tread or {@code null}. + * @param threadGroup the thread group to add the thread into or C {@code NULL} pointer. + * @return the current thread {@link JNIEnv} or C {@code NULL} pointer in case of error. + */ + public static JNIEnv attachCurrentThread(JavaVM vm, boolean daemon, String name, JObject threadGroup) { + try (CCharPointerHolder cname = CTypeConversion.toCString(name)) { + JavaVMAttachArgs args = StackValue.get(JavaVMAttachArgs.class); + args.setVersion(JNI_VERSION_10); + args.setGroup(threadGroup); + args.setName(cname.get()); + return daemon ? AttachCurrentThreadAsDaemon(vm, args) : AttachCurrentThread(vm, args); + } + } + + /*----------------- TRACING ------------------*/ + + public static boolean tracingAt(int level) { + return NativeBridgeSupport.getInstance().isTracingEnabled(level); + } + + /** + * Emits a trace line composed of {@code format} and {@code args} if the tracing level equal to + * or greater than {@code level}. + */ + public static void trace(int level, String format, Object... args) { + if (tracingAt(level)) { + NativeBridgeSupport.getInstance().trace(String.format(format, args)); + } + } + + public static void trace(int level, Throwable throwable) { + if (tracingAt(level)) { + StringWriter stringWriter = new StringWriter(); + try (PrintWriter out = new PrintWriter(stringWriter)) { + throwable.printStackTrace(out); + } + trace(level, stringWriter.toString()); + } + } + + static String getFeatureName() { + return NativeBridgeSupport.getInstance().getFeatureName(); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/NativeBridgeSupport.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/NativeBridgeSupport.java new file mode 100644 index 000000000000..3c734cc6e52f --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/jniutils/NativeBridgeSupport.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.jniutils; + +import org.graalvm.nativeimage.ImageSingletons; + +/** + * Services used by the {@code com.oracle.svm.jdwp.bridge.jniutils} module. To enable the + * {@code com.oracle.svm.jdwp.bridge.jniutils} module a {@code NativeBridgeSupport} instance must be + * registered in the {@link ImageSingletons}. + */ +public interface NativeBridgeSupport { + + /** + * Returns the name of a feature using {@code com.oracle.svm.jdwp.bridge.jniutils} module. The + * feature name is used in the logging output. + */ + String getFeatureName(); + + /** + * Checks if logging at given level is enabled. + */ + boolean isTracingEnabled(int level); + + /** + * Logs the message. + */ + void trace(String message); + + /** + * Returns a {@code NativeBridgeSupport} instance registered in the {@link ImageSingletons}. + */ + static NativeBridgeSupport getInstance() { + return ImageSingletons.lookup(NativeBridgeSupport.class); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryInput.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryInput.java new file mode 100644 index 000000000000..064160e38784 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryInput.java @@ -0,0 +1,640 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.ARRAY; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.BOOLEAN; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.BYTE; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.CHAR; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.DOUBLE; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.FLOAT; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.INT; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.LARGE_STRING_TAG; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.LONG; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.NULL; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.SHORT; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.STRING; +import static com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.bufferSize; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; + +/** + * A buffer used by the {@link BinaryMarshaller} to unmarshal parameters and results passed by + * value. + * + * @see BinaryOutput + * @see BinaryMarshaller + * @see JNIConfig.Builder#registerMarshaller(Class, BinaryMarshaller) + */ +public abstract class BinaryInput { + + private static final int EOF = -1; + + private byte[] tempEncodingByteBuffer; + private char[] tempEncodingCharBuffer; + protected final int length; + protected int pos; + + private BinaryInput(int length) { + this.length = length; + } + + /** + * Reads a single byte and returns {@code true} if that byte is non-zero, {@code false} if that + * byte is zero. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + */ + public final boolean readBoolean() throws IndexOutOfBoundsException { + int b = read(); + if (b < 0) { + throw new IndexOutOfBoundsException(); + } + return b != 0; + } + + /** + * Reads and returns a single byte. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + */ + public final byte readByte() throws IndexOutOfBoundsException { + int b = read(); + if (b < 0) { + throw new IndexOutOfBoundsException(); + } + return (byte) b; + } + + /** + * Reads two bytes and returns a {@code short} value. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + */ + public final short readShort() throws IndexOutOfBoundsException { + int b1 = read(); + int b2 = read(); + if ((b1 | b2) < 0) { + throw new IndexOutOfBoundsException(); + } + return packShort(b1, b2); + } + + /** + * Creates a Java {@code short} from given unsigned bytes, where {@code b1} is the most + * significant byte {@code byte} and {@code b2} is the least significant {@code byte}. + */ + private static short packShort(int b1, int b2) { + return (short) ((b1 << 8) + b2); + } + + /** + * Reads two bytes and returns a {@code char} value. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + */ + public final char readChar() throws IndexOutOfBoundsException { + int b1 = read(); + int b2 = read(); + if ((b1 | b2) < 0) { + throw new IndexOutOfBoundsException(); + } + return packChar(b1, b2); + } + + /** + * Creates a Java {@code char} from given unsigned bytes, where {@code b1} is the most + * significant byte {@code byte} and {@code b2} is the least significant {@code byte}. + */ + private static char packChar(int b1, int b2) { + return (char) ((b1 << 8) + b2); + } + + /** + * Reads four bytes and returns an {@code int} value. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + */ + public final int readInt() throws IndexOutOfBoundsException { + int b1 = read(); + int b2 = read(); + int b3 = read(); + int b4 = read(); + if ((b1 | b2 | b3 | b4) < 0) { + throw new IndexOutOfBoundsException(); + } + return packInt(b1, b2, b3, b4); + } + + /** + * Creates a Java {@code int} from given unsigned bytes, where {@code b1} is the most + * significant byte {@code byte} and {@code b4} is the least significant {@code byte}. + */ + private static int packInt(int b1, int b2, int b3, int b4) { + return (b1 << 24) + (b2 << 16) + (b3 << 8) + b4; + } + + /** + * Reads eight bytes and returns a {@code long} value. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + */ + public final long readLong() throws IndexOutOfBoundsException { + int b1 = read(); + int b2 = read(); + int b3 = read(); + int b4 = read(); + int b5 = read(); + int b6 = read(); + int b7 = read(); + int b8 = read(); + if ((b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8) < 0) { + throw new IndexOutOfBoundsException(); + } + return packLong(b1, b2, b3, b4, b5, b6, b7, b8); + } + + /** + * Creates a Java {@code long} from given unsigned bytes, where {@code b1} is the most + * significant byte {@code byte} and {@code b8} is the least significant {@code byte}. + */ + private static long packLong(int b1, int b2, int b3, int b4, int b5, int b6, int b7, int b8) { + return ((long) b1 << 56) + ((long) b2 << 48) + ((long) b3 << 40) + ((long) b4 << 32) + + ((long) b5 << 24) + ((long) b6 << 16) + ((long) b7 << 8) + b8; + } + + /** + * Reads four bytes and returns a {@code float} value. It does this by reading an {@code int} + * value and converting the {@code int} value to a {@code float} using + * {@link Float#intBitsToFloat(int)}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + */ + public final float readFloat() throws IndexOutOfBoundsException { + return Float.intBitsToFloat(readInt()); + } + + /** + * Reads eight bytes and returns a {@code double} value. It does this by reading a {@code long} + * value and converting the {@code long} value to a {@code double} using + * {@link Double#longBitsToDouble(long)}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + */ + public final double readDouble() throws IndexOutOfBoundsException { + return Double.longBitsToDouble(readLong()); + } + + /** + * Reads a single byte. The byte value is returned as an {@code int} in the range {@code 0} to + * {@code 255}. If no byte is available because the end of the stream has been reached, the + * value {@code -1} is returned. + */ + public abstract int read(); + + /** + * Reads {@code len} bytes into a byte array starting at offset {@code off}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read + */ + public abstract void read(byte[] b, int off, int len) throws IndexOutOfBoundsException; + + /** + * Reads a string using a modified UTF-8 encoding in a machine-independent manner. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + * @throws IllegalArgumentException if the bytes do not represent a valid modified UTF-8 + * encoding of a string. + */ + public final String readUTF() throws IndexOutOfBoundsException, IllegalArgumentException { + int len; + int b1 = read(); + int b2 = read(); + if ((b1 | b2) < 0) { + throw new IndexOutOfBoundsException(); + } + if ((b1 & LARGE_STRING_TAG) == LARGE_STRING_TAG) { + int b3 = read(); + int b4 = read(); + if ((b3 | b4) < 0) { + throw new IndexOutOfBoundsException(); + } + len = ((b1 & ~LARGE_STRING_TAG) << 24) + (b2 << 16) + (b3 << 8) + b4; + } else { + len = (b1 << 8) + b2; + } + ensureBufferSize(len); + if (tempEncodingCharBuffer == null || tempEncodingCharBuffer.length < len) { + tempEncodingCharBuffer = new char[Math.max(bufferSize(0, len), 80)]; + } + + int c1; + int c2; + int c3; + int byteCount = 0; + int charCount = 0; + + read(tempEncodingByteBuffer, 0, len); + + while (byteCount < len) { + c1 = tempEncodingByteBuffer[byteCount] & 0xff; + if (c1 > 127) { + break; + } + byteCount++; + tempEncodingCharBuffer[charCount++] = (char) c1; + } + + while (byteCount < len) { + c1 = tempEncodingByteBuffer[byteCount] & 0xff; + switch (c1 >> 4) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + /* 0xxxxxxx */ + byteCount++; + tempEncodingCharBuffer[charCount++] = (char) c1; + break; + case 12: + case 13: + /* 110x xxxx 10xx xxxx */ + byteCount += 2; + if (byteCount > len) { + throw new IllegalArgumentException("Partial character at end"); + } + c2 = tempEncodingByteBuffer[byteCount - 1]; + if ((c2 & 0xC0) != 0x80) { + throw new IllegalArgumentException("Malformed input around byte " + byteCount); + } + tempEncodingCharBuffer[charCount++] = (char) (((c1 & 0x1F) << 6) | (c2 & 0x3F)); + break; + case 14: + /* 1110 xxxx 10xx xxxx 10xx xxxx */ + byteCount += 3; + if (byteCount > len) { + throw new IllegalArgumentException("Malformed input: partial character at end"); + } + c2 = tempEncodingByteBuffer[byteCount - 2]; + c3 = tempEncodingByteBuffer[byteCount - 1]; + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) { + throw new IllegalArgumentException("Malformed input around byte " + (byteCount - 1)); + } + tempEncodingCharBuffer[charCount++] = (char) (((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F)); + break; + default: + /* 10xx xxxx, 1111 xxxx */ + throw new IllegalArgumentException("Malformed input around byte " + byteCount); + } + } + // The number of chars produced may be less than len + return new String(tempEncodingCharBuffer, 0, charCount); + } + + /** + * Reads a single value, using the data type encoded in the marshalled data. + * + * @return The read value, such as a boxed Java primitive, a {@link String}, a {@code null}, or + * an array of these types. + * @throws IndexOutOfBoundsException if there are not enough bytes to read. + * @throws IllegalArgumentException when the marshaled type is not supported or if the bytes do + * not represent a valid modified UTF-8 encoding of a string. + */ + public final Object readTypedValue() throws IndexOutOfBoundsException, IllegalArgumentException { + byte tag = readByte(); + switch (tag) { + case ARRAY: + int len = readInt(); + Object[] arr = new Object[len]; + for (int i = 0; i < len; i++) { + arr[i] = readTypedValue(); + } + return arr; + case NULL: + return null; + case BOOLEAN: + return readBoolean(); + case BYTE: + return readByte(); + case SHORT: + return readShort(); + case CHAR: + return readChar(); + case INT: + return readInt(); + case LONG: + return readLong(); + case FLOAT: + return readFloat(); + case DOUBLE: + return readDouble(); + case STRING: + return readUTF(); + default: + throw new IllegalArgumentException(String.format("Unknown tag %d", tag)); + } + } + + /** + * Reads {@code len} bytes into a boolean array starting at offset {@code off}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read + */ + public final void read(boolean[] b, int off, int len) { + ensureBufferSize(len); + read(tempEncodingByteBuffer, 0, len); + int limit = off + len; + for (int i = off, j = 0; i < limit; i++) { + b[i] = tempEncodingByteBuffer[j++] != 0; + } + } + + /** + * Reads {@code len} shorts into a short array starting at offset {@code off}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read + */ + public final void read(short[] b, int off, int len) { + int size = len * Short.BYTES; + ensureBufferSize(size); + read(tempEncodingByteBuffer, 0, size); + int limit = off + len; + for (int i = off, j = 0; i < limit; i++) { + int b1 = (tempEncodingByteBuffer[j++] & 0xff); + int b2 = (tempEncodingByteBuffer[j++] & 0xff); + b[i] = packShort(b1, b2); + } + } + + /** + * Reads {@code len} chars into a char array starting at offset {@code off}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read + */ + public final void read(char[] b, int off, int len) { + int size = len * Character.BYTES; + ensureBufferSize(size); + read(tempEncodingByteBuffer, 0, size); + int limit = off + len; + for (int i = off, j = 0; i < limit; i++) { + int b1 = (tempEncodingByteBuffer[j++] & 0xff); + int b2 = (tempEncodingByteBuffer[j++] & 0xff); + b[i] = packChar(b1, b2); + } + } + + /** + * Reads {@code len} ints into an int array starting at offset {@code off}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read + */ + public final void read(int[] b, int off, int len) { + int size = len * Integer.BYTES; + ensureBufferSize(size); + read(tempEncodingByteBuffer, 0, size); + int limit = off + len; + for (int i = off, j = 0; i < limit; i++) { + int b1 = (tempEncodingByteBuffer[j++] & 0xff); + int b2 = (tempEncodingByteBuffer[j++] & 0xff); + int b3 = (tempEncodingByteBuffer[j++] & 0xff); + int b4 = (tempEncodingByteBuffer[j++] & 0xff); + b[i] = packInt(b1, b2, b3, b4); + } + } + + /** + * Reads {@code len} longs into a long array starting at offset {@code off}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read + */ + public final void read(long[] b, int off, int len) { + int size = len * Long.BYTES; + ensureBufferSize(size); + read(tempEncodingByteBuffer, 0, size); + int limit = off + len; + for (int i = off, j = 0; i < limit; i++) { + int b1 = (tempEncodingByteBuffer[j++] & 0xff); + int b2 = (tempEncodingByteBuffer[j++] & 0xff); + int b3 = (tempEncodingByteBuffer[j++] & 0xff); + int b4 = (tempEncodingByteBuffer[j++] & 0xff); + int b5 = (tempEncodingByteBuffer[j++] & 0xff); + int b6 = (tempEncodingByteBuffer[j++] & 0xff); + int b7 = (tempEncodingByteBuffer[j++] & 0xff); + int b8 = (tempEncodingByteBuffer[j++] & 0xff); + b[i] = packLong(b1, b2, b3, b4, b5, b6, b7, b8); + } + } + + /** + * Reads {@code len} floats into a float array starting at offset {@code off}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read + */ + public final void read(float[] b, int off, int len) { + int size = len * Float.BYTES; + ensureBufferSize(size); + read(tempEncodingByteBuffer, 0, size); + int limit = off + len; + for (int i = off, j = 0; i < limit; i++) { + int b1 = (tempEncodingByteBuffer[j++] & 0xff); + int b2 = (tempEncodingByteBuffer[j++] & 0xff); + int b3 = (tempEncodingByteBuffer[j++] & 0xff); + int b4 = (tempEncodingByteBuffer[j++] & 0xff); + b[i] = Float.intBitsToFloat(packInt(b1, b2, b3, b4)); + } + } + + /** + * Reads {@code len} doubles into a double array starting at offset {@code off}. + * + * @throws IndexOutOfBoundsException if there are not enough bytes to read + */ + public final void read(double[] b, int off, int len) { + int size = len * Double.BYTES; + ensureBufferSize(size); + read(tempEncodingByteBuffer, 0, size); + int limit = off + len; + for (int i = off, j = 0; i < limit; i++) { + int b1 = (tempEncodingByteBuffer[j++] & 0xff); + int b2 = (tempEncodingByteBuffer[j++] & 0xff); + int b3 = (tempEncodingByteBuffer[j++] & 0xff); + int b4 = (tempEncodingByteBuffer[j++] & 0xff); + int b5 = (tempEncodingByteBuffer[j++] & 0xff); + int b6 = (tempEncodingByteBuffer[j++] & 0xff); + int b7 = (tempEncodingByteBuffer[j++] & 0xff); + int b8 = (tempEncodingByteBuffer[j++] & 0xff); + b[i] = Double.longBitsToDouble(packLong(b1, b2, b3, b4, b5, b6, b7, b8)); + } + } + + /** + * Returns a read only {@link ByteBuffer} backed by the {@link BinaryInput} internal buffer. The + * content of the buffer will start at the {@link BinaryInput}'s current position. The buffer's + * capacity and limit will be {@code len}, its position will be zero, its mark will be + * undefined, and its byte order will be {@link ByteOrder#BIG_ENDIAN BIG_ENDIAN}. After a + * successful call, the {@link BinaryInput}'s current position is incremented by the + * {@code len}. + * + * @throws IndexOutOfBoundsException if the BinaryInput has not enough remaining bytes. + */ + public abstract ByteBuffer asByteBuffer(int len); + + /** + * Creates a new buffer backed by a byte array. + */ + public static BinaryInput create(byte[] buffer) { + return new ByteArrayBinaryInput(buffer); + } + + /** + * Creates a new buffer backed by a byte array only up to a given length. + */ + public static BinaryInput create(byte[] buffer, int length) { + return new ByteArrayBinaryInput(buffer, length); + } + + /** + * Creates a new buffer wrapping an off-heap memory segment starting at an {@code address} + * having {@code length} bytes. + */ + public static BinaryInput create(CCharPointer address, int length) { + return new CCharPointerInput(address, length); + } + + private void ensureBufferSize(int len) { + if (tempEncodingByteBuffer == null || tempEncodingByteBuffer.length < len) { + tempEncodingByteBuffer = new byte[Math.max(bufferSize(0, len), 80)]; + } + } + + private static final class ByteArrayBinaryInput extends BinaryInput { + + private final byte[] buffer; + + ByteArrayBinaryInput(byte[] buffer) { + super(buffer.length); + this.buffer = buffer; + } + + ByteArrayBinaryInput(byte[] buffer, int length) { + super(length); + this.buffer = buffer; + } + + @Override + public int read() { + if (pos >= length) { + return EOF; + } + return (buffer[pos++] & 0xff); + } + + @Override + public void read(byte[] b, int off, int len) { + if (len < 0) { + throw new IllegalArgumentException(String.format("Len must be non negative but was %d", len)); + } + if (pos + len > length) { + throw new IndexOutOfBoundsException(); + } + System.arraycopy(buffer, pos, b, off, len); + pos += len; + } + + @Override + public ByteBuffer asByteBuffer(int len) { + ByteBuffer result = ByteBuffer.wrap(buffer, pos, len).slice().asReadOnlyBuffer(); + pos += len; + return result; + } + } + + private static final class CCharPointerInput extends BinaryInput { + + /** + * Represents the point at which the average cost of a JNI call exceeds the expense of an + * element by element copy. See {@code java.nio.Bits#JNI_COPY_TO_ARRAY_THRESHOLD}. + */ + private static final int BYTEBUFFER_COPY_TO_ARRAY_THRESHOLD = 6; + + private final CCharPointer address; + /** + * ByteBuffer view of this {@link CCharPointerInput} direct memory. The ByteBuffer is used + * for bulk data transfers, where the bulk ByteBuffer operations outperform element by + * element copying by an order of magnitude. + */ + private ByteBuffer byteBufferView; + + CCharPointerInput(CCharPointer address, int length) { + super(length); + this.address = address; + } + + @Override + public int read() { + if (pos >= length) { + return EOF; + } + return (address.read(pos++) & 0xff); + } + + @Override + public void read(byte[] b, int off, int len) { + if (len < 0) { + throw new IllegalArgumentException(String.format("Len must be non negative but was %d", len)); + } + if (pos + len > length) { + throw new IndexOutOfBoundsException(); + } + if (len > BYTEBUFFER_COPY_TO_ARRAY_THRESHOLD) { + if (byteBufferView == null) { + byteBufferView = CTypeConversion.asByteBuffer(address, length); + } + byteBufferView.position(pos); + byteBufferView.get(b, off, len); + } else { + for (int i = 0, j = pos; i < len; i++, j++) { + b[off + i] = address.read(j); + } + } + pos += len; + } + + @Override + public ByteBuffer asByteBuffer(int len) { + ByteBuffer result = CTypeConversion.asByteBuffer(address.addressOf(pos), len).order(ByteOrder.BIG_ENDIAN).asReadOnlyBuffer(); + pos += len; + return result; + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryMarshaller.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryMarshaller.java new file mode 100644 index 000000000000..69068014ee28 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryMarshaller.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.util.Objects; + +/** + * A marshaller used by the native bridge processor to read or write method parameters and results + * of a custom type. Marshallers are used to support types that are not directly implemented by the + * native bridge processor. + * + * @see JNIConfig.Builder + */ +public interface BinaryMarshaller { + + /** + * Decomposes and serializes the given object passed to a foreign method. + */ + void write(BinaryOutput output, T object); + + /** + * Deserializes and recreates an object passed to a foreign method. + */ + T read(BinaryInput input); + + /** + * Estimates a size in bytes needed to marshall given object. The returned value is used to + * pre-allocate the {@link BinaryOutput}'s buffer. The accuracy of the estimate affects the + * speed of marshalling. If the estimate is too small, the pre-allocated buffer must be + * re-allocated and the already marshalled data must be copied. Too large a value may cause the + * static buffer to be unused and the dynamic buffer to be unnecessarily allocated. + */ + default int inferSize(@SuppressWarnings("unused") T object) { + return Long.BYTES; + } + + /** + * Decomposes and serializes the mutable state of a given object to support {@link Out} + * semantics. Marshallers that do not support {@link Out} parameters do not need to implement + * this method. The default implementation throws {@link UnsupportedOperationException}. To + * support {@link Out} parameters the {@link BinaryMarshaller} must implement also + * {@link #readUpdate(BinaryInput, Object)} and {@link #inferUpdateSize(Object)}. + *

+ * The {@link Out} parameters are passed in the following way: + *

    + *
  1. The start point method writes the parameter using + * {@link #write(BinaryOutput, Object)}.
  2. + *
  3. A foreign method call is made.
  4. + *
  5. The end point method reads the parameter using {@link #read(BinaryInput)}.
  6. + *
  7. The end point receiver method is called with the unmarshalled parameter.
  8. + *
  9. After calling the receiver method, the end point method writes the mutated {@link Out} + * parameter state using {@link #writeUpdate(BinaryOutput, Object)}.
  10. + *
  11. A foreign method returns.
  12. + *
  13. The state of the {@link Out} parameter is updated using + * {@link #readUpdate(BinaryInput, Object)}.
  14. + *
+ *

+ * + * @see BinaryMarshaller#readUpdate(BinaryInput, Object) + * @see BinaryMarshaller#inferUpdateSize(Object) + */ + @SuppressWarnings("unused") + default void writeUpdate(BinaryOutput output, T object) { + throw new UnsupportedOperationException(); + } + + /** + * Deserializes and updates the mutable state of a given object to support {@link Out} + * semantics. Marshallers that do not support {@link Out} parameters do not need to implement + * this method. The default implementation throws {@link UnsupportedOperationException}. To + * support {@link Out} parameters the {@link BinaryMarshaller} must implement also + * {@link #writeUpdate(BinaryOutput, Object)} and {@link #inferUpdateSize(Object)}. + * + * @see BinaryMarshaller#writeUpdate(BinaryOutput, Object) + * @see BinaryMarshaller#inferUpdateSize(Object) + */ + @SuppressWarnings("unused") + default void readUpdate(BinaryInput input, T object) { + throw new UnsupportedOperationException(); + } + + /** + * Estimates a size in bytes needed to marshall {@link Out} parameter passed back to caller from + * a foreign method call. The accuracy of the estimate affects the speed of marshalling. If the + * estimate is too small, the pre-allocated buffer must be re-allocated and the already + * marshalled data must be copied. Too large a value may cause the static buffer to be unused + * and the dynamic buffer to be unnecessarily allocated. Marshallers that do not support + * {@link Out} parameters do not need to implement this method. The default implementation + * throws {@link UnsupportedOperationException}. To support {@link Out} parameters the + * {@link BinaryMarshaller} must implement also {@link #writeUpdate(BinaryOutput, Object)} and + * {@link #readUpdate(BinaryInput, Object)}. + * + * @see BinaryMarshaller#writeUpdate(BinaryOutput, Object) + * @see BinaryMarshaller#readUpdate(BinaryInput, Object) + */ + default int inferUpdateSize(@SuppressWarnings("unused") T object) { + throw new UnsupportedOperationException(); + } + + /** + * Decorates {@code forMarshaller} by a {@link BinaryMarshaller} handling {@code null} values. + * The returned {@link BinaryMarshaller} calls the {@code forMarshaller} only non-null values. + */ + static BinaryMarshaller nullable(BinaryMarshaller forMarshaller) { + Objects.requireNonNull(forMarshaller, "ForMarshaller must be non null."); + return new NullableBinaryMarshaller<>(forMarshaller); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryOutput.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryOutput.java new file mode 100644 index 000000000000..3544f3fe5947 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/BinaryOutput.java @@ -0,0 +1,670 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Objects; + +import org.graalvm.nativeimage.UnmanagedMemory; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.WordFactory; + +/** + * A buffer used by the {@link BinaryMarshaller} to marshall parameters and results passed by value. + * + * @see BinaryInput + * @see BinaryMarshaller + */ +public abstract class BinaryOutput { + + /** + * Maximum string length for string encoded by 4 bytes length followed be content. + */ + private static final int MAX_LENGTH = Integer.MAX_VALUE - Integer.BYTES; + /** + * Maximum string length for string encoded by 2 bytes length followed be content. + */ + private static final int MAX_SHORT_LENGTH = Short.MAX_VALUE; + /** + * Tag to distinguish between long and short string. The tag is set in the first string length + * byte. + */ + static final int LARGE_STRING_TAG = 1 << 7; + + // Type tags used by writeTypedValue + static final byte NULL = 0; + static final byte BOOLEAN = NULL + 1; + static final byte BYTE = BOOLEAN + 1; + static final byte SHORT = BYTE + 1; + static final byte CHAR = SHORT + 1; + static final byte INT = CHAR + 1; + static final byte LONG = INT + 1; + static final byte FLOAT = LONG + 1; + static final byte DOUBLE = FLOAT + 1; + static final byte STRING = DOUBLE + 1; + static final byte ARRAY = STRING + 1; + + private byte[] tempDecodingBuffer; + protected int pos; + + private BinaryOutput() { + } + + /** + * Writes a {@code boolean} as a single byte value. The value {@code true} is written as the + * value {@code (byte)1}, the value {@code false} is written as the value {@code (byte)0}. The + * buffer position is incremented by {@code 1}. + */ + public final void writeBoolean(boolean value) { + write(value ? 1 : 0); + } + + /** + * Writes a {@code byte} as a single byte value. The buffer position is incremented by + * {@code 1}. + */ + public final void writeByte(int value) { + write(value); + } + + /** + * Writes a {@code short} as two bytes, high byte first. The buffer position is incremented by + * {@code 2}. + */ + public final void writeShort(int value) { + write((value >>> 8) & 0xff); + write(value & 0xff); + } + + /** + * Writes a {@code char} as two bytes, high byte first. The buffer position is incremented by + * {@code 2}. + */ + public final void writeChar(int value) { + write((value >>> 8) & 0xff); + write(value & 0xff); + } + + /** + * Writes an {@code int} as four bytes, high byte first. The buffer position is incremented by + * {@code 4}. + */ + public final void writeInt(int value) { + write((value >>> 24) & 0xff); + write((value >>> 16) & 0xff); + write((value >>> 8) & 0xff); + write(value & 0xff); + } + + /** + * Writes a {@code long} as eight bytes, high byte first. The buffer position is incremented by + * {@code 8}. + */ + public final void writeLong(long value) { + write((int) ((value >>> 56) & 0xff)); + write((int) ((value >>> 48) & 0xff)); + write((int) ((value >>> 40) & 0xff)); + write((int) ((value >>> 32) & 0xff)); + write((int) ((value >>> 24) & 0xff)); + write((int) ((value >>> 16) & 0xff)); + write((int) ((value >>> 8) & 0xff)); + write((int) (value & 0xff)); + } + + /** + * Converts a {@code float} value to an {@code int} using the + * {@link Float#floatToIntBits(float)}, and then writes that {@code int} as four bytes, high + * byte first. The buffer position is incremented by {@code 4}. + */ + public final void writeFloat(float value) { + writeInt(Float.floatToIntBits(value)); + } + + /** + * Converts a {@code double} value to a {@code long} using the + * {@link Double#doubleToLongBits(double)}, and then writes that {@code long} as eight bytes, + * high byte first. The buffer position is incremented by {@code 8}. + */ + public final void writeDouble(double value) { + writeLong(Double.doubleToLongBits(value)); + } + + /** + * Writes the lowest byte of the argument as a single byte value. The buffer position is + * incremented by {@code 1}. + */ + public abstract void write(int b); + + /** + * Writes {@code len} bytes from the byte {@code array} starting at offset {@code off}. The + * buffer position is incremented by {@code len}. + */ + public abstract void write(byte[] array, int off, int len); + + /** + * Reserves a buffer space. The reserved space can be used for out parameters. + * + * @param numberOfBytes number of bytes to reserve. + */ + public abstract void skip(int numberOfBytes); + + /** + * Writes a string using a modified UTF-8 encoding in a machine-independent manner. + * + * @throws IllegalArgumentException if the {@code string} cannot be encoded using modified UTF-8 + * encoding. + */ + public final void writeUTF(String string) throws IllegalArgumentException { + int len = string.length(); + long utfLen = 0; + int c; + int count = 0; + + for (int i = 0; i < len; i++) { + c = string.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + utfLen++; + } else if (c > 0x07FF) { + utfLen += 3; + } else { + utfLen += 2; + } + } + + if (utfLen > MAX_LENGTH) { + throw new IllegalArgumentException("String too long to encode, " + utfLen + " bytes"); + } + int headerSize; + if (utfLen > MAX_SHORT_LENGTH) { + headerSize = Integer.BYTES; + ensureBufferSize(headerSize, (int) utfLen); + tempDecodingBuffer[count++] = (byte) ((LARGE_STRING_TAG | (utfLen >>> 24)) & 0xff); + tempDecodingBuffer[count++] = (byte) ((utfLen >>> 16) & 0xFF); + } else { + headerSize = Short.BYTES; + ensureBufferSize(headerSize, (int) utfLen); + } + tempDecodingBuffer[count++] = (byte) ((utfLen >>> 8) & 0xFF); + tempDecodingBuffer[count++] = (byte) (utfLen & 0xFF); + + int i = 0; + for (; i < len; i++) { + c = string.charAt(i); + if (!((c >= 0x0001) && (c <= 0x007F))) { + break; + } + tempDecodingBuffer[count++] = (byte) c; + } + + for (; i < len; i++) { + c = string.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + tempDecodingBuffer[count++] = (byte) c; + } else if (c > 0x07FF) { + tempDecodingBuffer[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F)); + tempDecodingBuffer[count++] = (byte) (0x80 | ((c >> 6) & 0x3F)); + tempDecodingBuffer[count++] = (byte) (0x80 | (c & 0x3F)); + } else { + tempDecodingBuffer[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F)); + tempDecodingBuffer[count++] = (byte) (0x80 | (c & 0x3F)); + } + } + write(tempDecodingBuffer, 0, (int) (headerSize + utfLen)); + } + + /** + * Returns this buffer's position. + */ + public int getPosition() { + return pos; + } + + /** + * Returns true if a value is a typed value writable using + * {@link #writeTypedValue(Object)}, else false. + */ + public static boolean isTypedValue(Object value) { + if (value == null) { + return true; + } + return value instanceof Object[] || value instanceof Boolean || value instanceof Byte || + value instanceof Short || value instanceof Character || value instanceof Integer || + value instanceof Long || value instanceof Float || value instanceof Double || value instanceof String; + } + + /** + * Writes the value that is represented by the given object, together with information on the + * value's data type. Supported types are boxed Java primitive types, {@link String}, + * {@code null}, and arrays of these types. + * + * @throws IllegalArgumentException when the {@code value} type is not supported or the + * {@code value} is a string which cannot be encoded using modified UTF-8 encoding. + * @see #isTypedValue(Object) to find out whether a value can be serialized. + */ + public final void writeTypedValue(Object value) throws IllegalArgumentException { + if (value instanceof Object[] arr) { + writeByte(ARRAY); + writeInt(arr.length); + for (Object arrElement : arr) { + writeTypedValue(arrElement); + } + } else if (value == null) { + writeByte(NULL); + } else if (value instanceof Boolean) { + writeByte(BOOLEAN); + writeBoolean((boolean) value); + } else if (value instanceof Byte) { + writeByte(BYTE); + writeByte((byte) value); + } else if (value instanceof Short) { + writeByte(SHORT); + writeShort((short) value); + } else if (value instanceof Character) { + writeByte(CHAR); + writeChar((char) value); + } else if (value instanceof Integer) { + writeByte(INT); + writeInt((int) value); + } else if (value instanceof Long) { + writeByte(LONG); + writeLong((long) value); + } else if (value instanceof Float) { + writeByte(FLOAT); + writeFloat((float) value); + } else if (value instanceof Double) { + writeByte(DOUBLE); + writeDouble((double) value); + } else if (value instanceof String) { + writeByte(STRING); + writeUTF((String) value); + } else { + throw new IllegalArgumentException(String.format("Unsupported type %s", value.getClass())); + } + } + + /** + * Writes {@code len} bytes from the boolean {@code array} starting at offset {@code off}. The + * value {@code true} is written as the value {@code (byte)1}, the value {@code false} is + * written as the value {@code (byte)0}. The buffer position is incremented by {@code len}. + */ + public final void write(boolean[] array, int off, int len) { + ensureBufferSize(0, len); + for (int i = 0, j = 0; i < len; i++, j++) { + tempDecodingBuffer[j] = (byte) (array[off + i] ? 1 : 0); + } + write(tempDecodingBuffer, 0, len); + } + + /** + * Writes {@code len} shorts from the {@code array} starting at offset {@code off}. The buffer + * position is incremented by {@code 2 * len}. + */ + public final void write(short[] array, int off, int len) { + int size = len * Short.BYTES; + ensureBufferSize(0, size); + for (int i = 0, j = 0; i < len; i++) { + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 8) & 0xff); + tempDecodingBuffer[j++] = (byte) (array[off + i] & 0xff); + } + write(tempDecodingBuffer, 0, size); + } + + /** + * Writes {@code len} chars from the {@code array} starting at offset {@code off}. The buffer + * position is incremented by {@code 2 * len}. + */ + public final void write(char[] array, int off, int len) { + int size = len * Character.BYTES; + ensureBufferSize(0, size); + for (int i = 0, j = 0; i < len; i++) { + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 8) & 0xff); + tempDecodingBuffer[j++] = (byte) (array[off + i] & 0xff); + } + write(tempDecodingBuffer, 0, size); + } + + /** + * Writes {@code len} ints from the {@code array} starting at offset {@code off}. The buffer + * position is incremented by {@code 4 * len}. + */ + public final void write(int[] array, int off, int len) { + int size = len * Integer.BYTES; + ensureBufferSize(0, size); + for (int i = 0, j = 0; i < len; i++) { + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 24) & 0xff); + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 16) & 0xff); + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 8) & 0xff); + tempDecodingBuffer[j++] = (byte) (array[off + i] & 0xff); + } + write(tempDecodingBuffer, 0, size); + } + + /** + * Writes {@code len} longs from the {@code array} starting at offset {@code off}. The buffer + * position is incremented by {@code 8 * len}. + */ + public final void write(long[] array, int off, int len) { + int size = len * Long.BYTES; + ensureBufferSize(0, size); + for (int i = 0, j = 0; i < len; i++) { + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 56) & 0xff); + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 48) & 0xff); + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 40) & 0xff); + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 32) & 0xff); + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 24) & 0xff); + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 16) & 0xff); + tempDecodingBuffer[j++] = (byte) ((array[off + i] >>> 8) & 0xff); + tempDecodingBuffer[j++] = (byte) (array[off + i] & 0xff); + } + write(tempDecodingBuffer, 0, size); + } + + /** + * Writes {@code len} floats from the {@code array} starting at offset {@code off}. Each + * {@code float} value is converted to an {@code int} using the + * {@link Float#floatToIntBits(float)} and written as an int. The buffer position is incremented + * by {@code 4 * len}. + */ + public final void write(float[] array, int off, int len) { + int size = len * Float.BYTES; + ensureBufferSize(0, size); + for (int i = 0, j = 0; i < len; i++) { + int bits = Float.floatToIntBits(array[off + i]); + tempDecodingBuffer[j++] = (byte) ((bits >>> 24) & 0xff); + tempDecodingBuffer[j++] = (byte) ((bits >>> 16) & 0xff); + tempDecodingBuffer[j++] = (byte) ((bits >>> 8) & 0xff); + tempDecodingBuffer[j++] = (byte) (bits & 0xff); + } + write(tempDecodingBuffer, 0, size); + } + + /** + * Writes {@code len} doubles from the {@code array} starting at offset {@code off}. Each + * {@code double} value is converted to an {@code lang} using the + * {@link Double#doubleToLongBits(double)} and written as a long. The buffer position is + * incremented by {@code 8 * len}. + */ + public final void write(double[] array, int off, int len) { + int size = len * Double.BYTES; + ensureBufferSize(Integer.BYTES, size); + for (int i = 0, j = 0; i < len; i++) { + long bits = Double.doubleToLongBits(array[off + i]); + tempDecodingBuffer[j++] = (byte) ((bits >>> 56) & 0xff); + tempDecodingBuffer[j++] = (byte) ((bits >>> 48) & 0xff); + tempDecodingBuffer[j++] = (byte) ((bits >>> 40) & 0xff); + tempDecodingBuffer[j++] = (byte) ((bits >>> 32) & 0xff); + tempDecodingBuffer[j++] = (byte) ((bits >>> 24) & 0xff); + tempDecodingBuffer[j++] = (byte) ((bits >>> 16) & 0xff); + tempDecodingBuffer[j++] = (byte) ((bits >>> 8) & 0xff); + tempDecodingBuffer[j++] = (byte) (bits & 0xff); + } + write(tempDecodingBuffer, 0, size); + } + + private void ensureBufferSize(int headerSize, int dataSize) { + if (tempDecodingBuffer == null || tempDecodingBuffer.length < (headerSize + dataSize)) { + tempDecodingBuffer = new byte[bufferSize(headerSize, dataSize)]; + } + } + + /** + * Creates a new buffer backed by a byte array. + */ + public static ByteArrayBinaryOutput create() { + return new ByteArrayBinaryOutput(ByteArrayBinaryOutput.INITIAL_SIZE); + } + + /** + * Creates a new buffer wrapping the {@code initialBuffer}. If the {@code initialBuffer} + * capacity is not sufficient for writing the data, a new array is allocated. Always use + * {@link ByteArrayBinaryOutput#getArray()} to obtain the marshaled data. + */ + public static ByteArrayBinaryOutput create(byte[] initialBuffer) { + Objects.requireNonNull(initialBuffer, "InitialBuffer must be non null."); + return new ByteArrayBinaryOutput(initialBuffer); + } + + /** + * Creates a new buffer wrapping an off-heap memory segment starting at {@code address} having + * {@code length} bytes. If the capacity of an off-heap memory segment is not sufficient for + * writing the data, a new off-heap memory is allocated. Always use + * {@link CCharPointerBinaryOutput#getAddress()} to obtain the marshaled data. + * + * @param address the off-heap memory address + * @param length the off-heap memory size + * @param dynamicallyAllocated {@code true} if the memory was dynamically allocated and should + * be freed when the buffer is closed; {@code false} for the stack allocated memory. + */ + public static CCharPointerBinaryOutput create(CCharPointer address, int length, boolean dynamicallyAllocated) { + return new CCharPointerBinaryOutput(address, length, dynamicallyAllocated); + } + + static int bufferSize(int headerSize, int dataSize) { + return headerSize + (dataSize <= MAX_SHORT_LENGTH ? dataSize << 1 : dataSize); + } + + /** + * A {@link BinaryOutput} backed by a byte array. + */ + public static final class ByteArrayBinaryOutput extends BinaryOutput { + + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + static final int INITIAL_SIZE = 32; + + private byte[] buffer; + + private ByteArrayBinaryOutput(int size) { + buffer = new byte[size]; + } + + private ByteArrayBinaryOutput(byte[] initialBuffer) { + buffer = initialBuffer; + } + + @Override + public void write(int b) { + ensureCapacity(pos + 1); + buffer[pos] = (byte) b; + pos += 1; + } + + @Override + public void write(byte[] b, int off, int len) { + ensureCapacity(pos + len); + System.arraycopy(b, off, buffer, pos, len); + pos += len; + } + + @Override + public void skip(int numberOfBytes) { + ensureCapacity(pos + numberOfBytes); + pos += numberOfBytes; + } + + /** + * Returns the byte array containing the marshalled data. + */ + public byte[] getArray() { + return buffer; + } + + private void ensureCapacity(int neededCapacity) { + if (neededCapacity - buffer.length > 0) { + int newCapacity = buffer.length << 1; + if (newCapacity - neededCapacity < 0) { + newCapacity = neededCapacity; + } + if (newCapacity - MAX_ARRAY_SIZE > 0) { + throw new OutOfMemoryError(); + } + buffer = Arrays.copyOf(buffer, newCapacity); + } + } + + /** + * Creates a new buffer backed by a byte array. The buffer initial size is + * {@code initialSize}. + */ + public static ByteArrayBinaryOutput create(int initialSize) { + return new ByteArrayBinaryOutput(initialSize); + } + } + + /** + * A {@link BinaryOutput} backed by an off-heap memory. + */ + public static final class CCharPointerBinaryOutput extends BinaryOutput implements Closeable { + + /** + * Represents the point at which the average cost of a JNI call exceeds the expense of an + * element by element copy. See {@code java.nio.Bits#JNI_COPY_FROM_ARRAY_THRESHOLD}. + */ + private static final int BYTEBUFFER_COPY_FROM_ARRAY_THRESHOLD = 6; + + private CCharPointer address; + private int length; + private boolean unmanaged; + /** + * ByteBuffer view of this {@link CCharPointerBinaryOutput} direct memory. The ByteBuffer is + * used for bulk data transfers, where the bulk ByteBuffer operations outperform element by + * element copying by an order of magnitude. + */ + private ByteBuffer byteBufferView; + + private CCharPointerBinaryOutput(CCharPointer address, int length, boolean unmanaged) { + this.address = address; + this.length = length; + this.unmanaged = unmanaged; + } + + @Override + public int getPosition() { + checkClosed(); + return super.getPosition(); + } + + /** + * Returns an address of an off-heap memory segment containing the marshalled data. + */ + public CCharPointer getAddress() { + checkClosed(); + return address; + } + + @Override + public void write(int b) { + checkClosed(); + ensureCapacity(pos + 1); + address.write(pos++, (byte) b); + } + + @Override + public void write(byte[] b, int off, int len) { + checkClosed(); + if ((off | len | b.length) < 0 || b.length - off < len) { + throw new IndexOutOfBoundsException("Offset: " + off + ", length: " + len + ", array length: " + b.length); + } + ensureCapacity(pos + len); + if (len > BYTEBUFFER_COPY_FROM_ARRAY_THRESHOLD) { + if (byteBufferView == null) { + byteBufferView = CTypeConversion.asByteBuffer(address, length); + } + byteBufferView.position(pos); + byteBufferView.put(b, off, len); + } else { + for (int i = 0; i < len; i++) { + address.write(pos + i, b[off + i]); + } + } + pos += len; + } + + @Override + public void skip(int numberOfBytes) { + ensureCapacity(pos + numberOfBytes); + pos += numberOfBytes; + } + + /** + * Closes the buffer and frees off-heap allocated resources. + */ + @Override + public void close() { + if (unmanaged) { + UnmanagedMemory.free(address); + byteBufferView = null; + address = WordFactory.nullPointer(); + length = 0; + unmanaged = false; + pos = Integer.MIN_VALUE; + } + } + + private void checkClosed() { + if (pos == Integer.MIN_VALUE) { + throw new IllegalStateException("Already closed"); + } + } + + private void ensureCapacity(int neededCapacity) { + if (neededCapacity - length > 0) { + byteBufferView = null; + int newCapacity = length << 1; + if (newCapacity - neededCapacity < 0) { + newCapacity = neededCapacity; + } + if (newCapacity - Integer.MAX_VALUE > 0) { + throw new OutOfMemoryError(); + } + if (unmanaged) { + address = UnmanagedMemory.realloc(address, WordFactory.unsigned(newCapacity)); + } else { + CCharPointer newAddress = UnmanagedMemory.malloc(newCapacity); + memcpy(newAddress, address, pos); + address = newAddress; + } + length = newCapacity; + unmanaged = true; + } + } + + private static void memcpy(CCharPointer dst, CCharPointer src, int len) { + for (int i = 0; i < len; i++) { + dst.write(i, src.read(i)); + } + } + + /** + * Creates a new buffer backed by an off-heap memory segment. The buffer initial size is + * {@code initialSize}. + */ + public static CCharPointerBinaryOutput create(int initialSize) { + return new CCharPointerBinaryOutput(UnmanagedMemory.malloc(initialSize), initialSize, true); + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/DefaultStackTraceMarshaller.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/DefaultStackTraceMarshaller.java new file mode 100644 index 000000000000..241f73f0bb4a --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/DefaultStackTraceMarshaller.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +final class DefaultStackTraceMarshaller implements BinaryMarshaller { + + static final DefaultStackTraceMarshaller INSTANCE = new DefaultStackTraceMarshaller(); + + private DefaultStackTraceMarshaller() { + } + + @Override + public StackTraceElement[] read(BinaryInput in) { + int len = in.readInt(); + StackTraceElement[] res = new StackTraceElement[len]; + for (int i = 0; i < len; i++) { + res[i] = StackTraceElementMarshaller.INSTANCE.read(in); + } + return res; + } + + @Override + public void write(BinaryOutput out, StackTraceElement[] stack) { + out.writeInt(stack.length); + for (StackTraceElement stackTraceElement : stack) { + StackTraceElementMarshaller.INSTANCE.write(out, stackTraceElement); + } + } + + @Override + public int inferSize(StackTraceElement[] object) { + return object.length == 0 ? 0 : object.length * StackTraceElementMarshaller.INSTANCE.inferSize(object[0]); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/DefaultThrowableMarshaller.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/DefaultThrowableMarshaller.java new file mode 100644 index 000000000000..992afdf1b475 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/DefaultThrowableMarshaller.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +final class DefaultThrowableMarshaller implements BinaryMarshaller { + + private static final int THROWABLE_SIZE_ESTIMATE = 1024; + private final DefaultStackTraceMarshaller stackTraceMarshaller = DefaultStackTraceMarshaller.INSTANCE; + + @Override + public Throwable read(BinaryInput in) { + String foreignExceptionClassName = in.readUTF(); + String foreignExceptionMessage = (String) in.readTypedValue(); + StackTraceElement[] foreignExceptionStack = stackTraceMarshaller.read(in); + return new MarshalledException(foreignExceptionClassName, foreignExceptionMessage, ForeignException.mergeStackTrace(foreignExceptionStack)); + } + + @Override + public void write(BinaryOutput out, Throwable object) { + out.writeUTF(object instanceof MarshalledException ? ((MarshalledException) object).getForeignExceptionClassName() : object.getClass().getName()); + out.writeTypedValue(object.getMessage()); + stackTraceMarshaller.write(out, object.getStackTrace()); + } + + @Override + public int inferSize(Throwable object) { + // We don't use Throwable#getStackTrace as it allocates. + return THROWABLE_SIZE_ESTIMATE; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/ForeignException.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/ForeignException.java new file mode 100644 index 000000000000..90b4a5681bf1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/ForeignException.java @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import static com.oracle.svm.jdwp.bridge.jniutils.JNIUtil.GetStaticMethodID; +import static org.graalvm.nativeimage.c.type.CTypeConversion.toCString; + +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JByteArray; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JClass; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JMethodID; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JObject; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JThrowable; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JValue; +import com.oracle.svm.jdwp.bridge.jniutils.JNICalls; +import com.oracle.svm.jdwp.bridge.jniutils.JNIExceptionWrapper; +import com.oracle.svm.jdwp.bridge.jniutils.JNIUtil; +import com.oracle.svm.jdwp.bridge.nativebridge.BinaryOutput.ByteArrayBinaryOutput; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.type.CTypeConversion; + +/** + * This exception is used to transfer a local exception over the boundary. The local exception is + * marshaled, passed over the boundary as a {@link ForeignException}, unmarshalled, and re-thrown. + */ +@SuppressWarnings("serial") +public final class ForeignException extends RuntimeException { + + static final byte UNDEFINED = 0; + static final byte HOST_TO_GUEST = 1; + static final byte GUEST_TO_HOST = 2; + private static final ThreadLocal pendingException = new ThreadLocal<>(); + private static final JNIMethodResolver CreateForeignException = new JNIMethodResolver("createForeignException", "([B)Ljava/lang/Throwable;"); + private static final JNIMethodResolver ToByteArray = new JNIMethodResolver("toByteArray", "(Lcom/oracle/svm/jdwp/bridge/nativebridge/ForeignException;)[B"); + /** + * Pre-allocated {@code ForeignException} for double failure. + */ + private static final ForeignException MARSHALLING_FAILED = new ForeignException(null, UNDEFINED, false); + private static volatile JNICalls jniCalls; + + private final byte kind; + private final byte[] rawData; + + private ForeignException(byte[] rawData, byte kind, boolean writableStackTrace) { + super(null, null, true, writableStackTrace); + this.rawData = rawData; + this.kind = kind; + } + + /** + * Re-throws this exception in host using a JNI API. This method is intended to be called by the + * code generated by the bridge processor. It's still possible to use it by the hand-written + * code, but it's recommended to use the bridge processor. + */ + public void throwUsingJNI(JNIEnv env) { + JNIUtil.Throw(env, callCreateForeignException(env, JNIUtil.createHSArray(env, rawData))); + } + + /** + * Unmarshalls the foreign exception transferred by this {@link ForeignException} and re-throws + * it. This method is intended to be called by the code generated by the bridge processor. It's + * still possible to use it by the hand-written code, but it's recommended to use the bridge + * processor. + * + * @param marshaller the marshaller to unmarshal the exception + */ + public RuntimeException throwOriginalException(BinaryMarshaller marshaller) { + try { + if (rawData == null) { + throw new RuntimeException("Failed to marshall foreign throwable."); + } + BinaryInput in = BinaryInput.create(rawData); + Throwable t = marshaller.read(in); + throw ForeignException.silenceException(RuntimeException.class, t); + } finally { + clearPendingException(); + } + } + + /** + * Merges foreign stack trace marshalled in the {@link ForeignException} with local + * {@link ForeignException}'s stack trace. This is a helper method for throwable marshallers to + * merge local and foreign stack traces. Typical usage looks like this: + * + *

+     * final class DefaultThrowableMarshaller implements BinaryMarshaller<Throwable> {
+     *     private final BinaryMarshaller<StackTraceElement[]> stackTraceMarshaller = MyJNIConfig.getDefault().lookupMarshaller(StackTraceElement[].class);
+     *
+     *     @Override
+     *     public Throwable read(BinaryInput in) {
+     *         String foreignExceptionClassName = in.readUTF();
+     *         String foreignExceptionMessage = in.readUTF();
+     *         StackTraceElement[] foreignExceptionStack = stackTraceMarshaller.read(in);
+     *         RuntimeException exception = new RuntimeException(foreignExceptionClassName + ": " + foreignExceptionMessage);
+     *         exception.setStackTrace(ForeignException.mergeStackTrace(foreignExceptionStack));
+     *         return exception;
+     *     }
+     *
+     *     @Override
+     *     public void write(BinaryOutput out, Throwable object) {
+     *         out.writeUTF(object.getClass().getName());
+     *         out.writeUTF(object.getMessage());
+     *         stackTraceMarshaller.write(out, object.getStackTrace());
+     *     }
+     * }
+     * 
+ * + * @param foreignExceptionStack the stack trace marshalled into the {@link ForeignException} + * @return the stack trace combining both local and foreign stack trace elements + */ + public static StackTraceElement[] mergeStackTrace(StackTraceElement[] foreignExceptionStack) { + if (foreignExceptionStack.length == 0) { + // Exception has no stack trace, nothing to merge. + return foreignExceptionStack; + } + ForeignException localException = pendingException.get(); + if (localException != null) { + switch (localException.kind) { + case HOST_TO_GUEST: + return JNIExceptionWrapper.mergeStackTraces(localException.getStackTrace(), foreignExceptionStack, false); + case GUEST_TO_HOST: + return JNIExceptionWrapper.mergeStackTraces(foreignExceptionStack, localException.getStackTrace(), true); + default: + throw new IllegalStateException("Unsupported kind " + localException.kind); + } + } else { + return foreignExceptionStack; + } + } + + /** + * Creates a {@link ForeignException} by marshaling the {@code exception} using + * {@code marshaller}. This method is intended to be called by the code generated by the bridge + * processor. It's still possible to use it by the hand-written code, but it's recommended to + * use the bridge processor. + * + * @param exception the exception that should be passed over the boundary + * @param marshaller the marshaller to marshall the exception + */ + public static ForeignException forThrowable(Throwable exception, BinaryMarshaller marshaller) { + try { + ByteArrayBinaryOutput out = ByteArrayBinaryOutput.create(marshaller.inferSize(exception)); + marshaller.write(out, exception); + return new ForeignException(out.getArray(), UNDEFINED, false); + } catch (ThreadDeath td) { + throw td; + } catch (Throwable t) { + // Exception marshalling failed, prevent exception propagation from CEntryPoint that + // may cause process crash. + return MARSHALLING_FAILED; + } + } + + /** + * Returns the {@link JNICalls} transferring the {@link ForeignException} thrown in the HotSpot + * to an isolate. This method is intended to be called by the code generated by the bridge + * processor. It's still possible to use it by the hand-written code, but it's recommended to + * use the bridge processor. + */ + public static JNICalls getJNICalls() { + JNICalls res = jniCalls; + if (res == null) { + res = createJNICalls(); + jniCalls = res; + } + return res; + } + + byte[] toByteArray() { + return rawData; + } + + static ForeignException create(byte[] rawData, byte kind) { + ForeignException exception = new ForeignException(rawData, kind, true); + pendingException.set(exception); + return exception; + } + + private static void clearPendingException() { + pendingException.set(null); + } + + @SuppressWarnings({"unchecked", "unused"}) + private static T silenceException(Class type, Throwable t) throws T { + throw (T) t; + } + + private static JNICalls createJNICalls() { + return JNICalls.createWithExceptionHandler(context -> { + if (ForeignException.class.getName().equals(context.getThrowableClassName())) { + JNIEnv env = context.getEnv(); + byte[] marshalledData = JNIUtil.createArray(env, callToByteArray(env, context.getThrowable())); + throw ForeignException.create(marshalledData, ForeignException.GUEST_TO_HOST); + } else { + context.throwJNIExceptionWrapper(); + } + }); + } + + private static JThrowable callCreateForeignException(JNIEnv env, JByteArray rawValue) { + JValue args = StackValue.get(1, JValue.class); + args.addressOf(0).setJObject(rawValue); + return JNICalls.getDefault().callStaticJObject(env, CreateForeignException.getEntryPoints(env), CreateForeignException.resolve(env), args); + } + + private static JByteArray callToByteArray(JNIEnv env, JObject p0) { + JValue args = StackValue.get(1, JValue.class); + args.addressOf(0).setJObject(p0); + return JNICalls.getDefault().callStaticJObject(env, ToByteArray.getEntryPoints(env), ToByteArray.resolve(env), args); + } + + private static final class JNIMethodResolver implements JNICalls.JNIMethod { + + private final String methodName; + private final String methodSignature; + private volatile JClass entryPointsClass; + private volatile JMethodID methodId; + + JNIMethodResolver(String methodName, String methodSignature) { + this.methodName = methodName; + this.methodSignature = methodSignature; + } + + JNIMethodResolver resolve(JNIEnv jniEnv) { + JMethodID res = methodId; + if (res.isNull()) { + JClass entryPointClass = getEntryPoints(jniEnv); + try (CTypeConversion.CCharPointerHolder name = toCString(methodName); CTypeConversion.CCharPointerHolder sig = toCString(methodSignature)) { + res = GetStaticMethodID(jniEnv, entryPointClass, name.get(), sig.get()); + if (res.isNull()) { + throw new InternalError("No such method: " + methodName); + } + methodId = res; + } + } + return this; + } + + JClass getEntryPoints(JNIEnv env) { + JClass res = entryPointsClass; + if (res.isNull()) { + res = JNIClassCache.lookupClass(env, ForeignExceptionEndPoints.class); + entryPointsClass = res; + } + return res; + } + + @Override + public JMethodID getJMethodID() { + return methodId; + } + + @Override + public String getDisplayName() { + return methodName; + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/ForeignExceptionEndPoints.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/ForeignExceptionEndPoints.java new file mode 100644 index 000000000000..cfb50a5b7061 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/ForeignExceptionEndPoints.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import com.oracle.svm.jdwp.bridge.jniutils.JNIEntryPoint; + +final class ForeignExceptionEndPoints { + + private ForeignExceptionEndPoints() { + } + + /** + * Called by JNI to create a {@link ForeignException} used to throw native exception into Java + * code. + * + * @param rawValue marshalled original exception + * @return a {@link ForeignException} instance + */ + @JNIEntryPoint + static Throwable createForeignException(byte[] rawValue) { + return ForeignException.create(rawValue, ForeignException.HOST_TO_GUEST); + } + + /** + * Called by JNI to return a marshalled exception transferred by the {@code exception}. + */ + @JNIEntryPoint + static byte[] toByteArray(ForeignException exception) { + return exception.toByteArray(); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/In.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/In.java new file mode 100644 index 000000000000..864f32f3f1f7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/In.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Configures an array parameter as an in-parameter. For an in-parameter, the array value is copied + * over the boundary into a called method. The {@link In} is the default behavior. It's needed only + * in combination with {@link Out} for in-out parameters. + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.PARAMETER) +public @interface In { + + /** + * Copy only a part of the array starting at the offset given by the + * {@code arrayOffsetParameter} method parameter. By default, the whole array is copied. The + * {@code arrayOffsetParameter} can be used to improve the performance and copy only a part of + * the array over the boundary. + */ + String arrayOffsetParameter() default ""; + + /** + * Limits copying only to many of the elements given by the {@code arrayLengthParameter} + * parameter. By default, the whole array is copied. The {@code arrayLengthParameter} can be + * used to improve the performance and copy only a part of the array over the boundary. + */ + String arrayLengthParameter() default ""; +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/JNIClassCache.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/JNIClassCache.java new file mode 100644 index 000000000000..5bbbdf0734d7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/JNIClassCache.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JClass; +import com.oracle.svm.jdwp.bridge.jniutils.JNI.JNIEnv; +import com.oracle.svm.jdwp.bridge.jniutils.JNIExceptionWrapper; +import com.oracle.svm.jdwp.bridge.jniutils.JNIUtil; +import org.graalvm.word.WordFactory; + +/** + * Support class for {@link JClass} lookup. JClass instances are cached as JNI globals. The cached + * JNI globals are disposed by {@link JNIClassCache#dispose(JNIEnv)}. + */ +public final class JNIClassCache { + + private static final Map classesByName = new ConcurrentHashMap<>(); + + private JNIClassCache() { + } + + /** + * Looks up JClass using a {@link Class}. + * + * @return JNI global reference for {@link JClass} + * @throws JNIExceptionWrapper wrapping the HotSpot {@link LinkageError} is thrown when class is + * not found. + */ + public static JClass lookupClass(JNIEnv env, Class clazz) throws JNIExceptionWrapper { + return lookupClass(env, clazz.getName()); + } + + /** + * Looks up JClass using a fully qualified name. + * + * @return JNI global reference for {@link JClass} + * @throws JNIExceptionWrapper wrapping the HotSpot {@link LinkageError} is thrown when class is + * not found. + */ + public static JClass lookupClass(JNIEnv env, String className) throws JNIExceptionWrapper { + return lookupClassImpl(env, className, true); + } + + private static JClass lookupClassImpl(JNIEnv env, String className, boolean required) { + Function createClassData = new Function<>() { + @Override + public JNIClassData apply(String cn) { + JClass jClass = JNIUtil.findClass(env, WordFactory.nullPointer(), JNIUtil.getBinaryName(className), required); + if (jClass.isNull()) { + return JNIClassData.INVALID; + } + return new JNIClassData(JNIUtil.NewGlobalRef(env, jClass, "Class<" + className + ">")); + } + }; + return classesByName.computeIfAbsent(className, createClassData).jClassGlobal; + } + + /** + * Disposes cached JNI objects and frees JNI globals. The isolate should call this method before + * disposing to free host classes held by JNI global references. + */ + public static void dispose(JNIEnv jniEnv) { + for (Iterator iterator = classesByName.values().iterator(); iterator.hasNext();) { + JNIClassData classData = iterator.next(); + iterator.remove(); + if (classData != JNIClassData.INVALID) { + JNIUtil.DeleteGlobalRef(jniEnv, classData.jClassGlobal); + } + } + } + + private static final class JNIClassData { + + static final JNIClassData INVALID = new JNIClassData(null); + + private final JClass jClassGlobal; + + JNIClassData(JClass jClassGlobal) { + this.jClassGlobal = jClassGlobal; + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/JNIConfig.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/JNIConfig.java new file mode 100644 index 000000000000..b60e57b585f9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/JNIConfig.java @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +// Checkstyle: allow direct annotation access +// Checkstyle: allow Class.getSimpleName +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.LongBinaryOperator; +import java.util.function.LongUnaryOperator; +import java.util.function.Supplier; + +import org.graalvm.collections.Pair; + +/** + * A configuration used by the {@link NativeIsolate} and classes generated by the native bridge + * processor. + */ +public final class JNIConfig { + + private final Map> binaryMarshallers; + private final Map, List, BinaryMarshaller>>> annotationBinaryMarshallers; + private final LongUnaryOperator attachThreadAction; + private final LongUnaryOperator detachThreadAction; + private final LongBinaryOperator shutDownIsolateAction; + private final LongBinaryOperator releaseNativeObjectAction; + private final Function, ThreadLocal> threadLocalFactory; + + JNIConfig(Map> binaryMarshallers, + Map, List, BinaryMarshaller>>> annotationBinaryMarshallers, + LongUnaryOperator attachThreadAction, LongUnaryOperator detachThreadAction, + LongBinaryOperator shutDownIsolateAction, LongBinaryOperator releaseNativeObjectAction, + Function, ThreadLocal> threadLocalFactory) { + this.binaryMarshallers = binaryMarshallers; + this.annotationBinaryMarshallers = annotationBinaryMarshallers; + this.attachThreadAction = attachThreadAction; + this.detachThreadAction = detachThreadAction; + this.shutDownIsolateAction = shutDownIsolateAction; + this.releaseNativeObjectAction = releaseNativeObjectAction; + this.threadLocalFactory = threadLocalFactory; + } + + /** + * Looks up {@link BinaryMarshaller} for the {@code type} and {@code annotationTypes}. The + * method first tries to find a marshaller registered for the {@code type} and some annotation + * from {@code annotationTypes}. If no such marshaller exists, it tries to find a marshaller + * registered just for the {@code type}. If there is no such a marshaller it throws the + * {@link UnsupportedOperationException}. + * + * @param type the parameter or return type. + * @param annotationTypes parameter or method annotation types. + * @throws UnsupportedOperationException if there is no registered marshaller for the + * {@code type}. + */ + @SuppressWarnings("unchecked") + @SafeVarargs + public final BinaryMarshaller lookupMarshaller(Class type, Class... annotationTypes) { + BinaryMarshaller res = lookupBinaryMarshallerImpl(type, annotationTypes); + if (res != null) { + return (BinaryMarshaller) res; + } else { + throw unsupported(type); + } + } + + /** + * Looks up {@link BinaryMarshaller} for the {@code parameterizedType} and + * {@code annotationTypes}. The method first tries to find a marshaller registered for the + * {@code parameterizedType} and some annotation from {@code annotationTypes}. If no such + * marshaller exists, it tries to find a marshaller registered just for the + * {@code parameterizedType}. If there is no such a marshaller it throws the + * {@link UnsupportedOperationException}. + * + * @param parameterizedType the parameter or return type. + * @param annotationTypes parameter or method annotation types. + * @throws UnsupportedOperationException if there is no registered marshaller for the + * {@code parameterizedType}. + */ + @SuppressWarnings("unchecked") + @SafeVarargs + public final BinaryMarshaller lookupMarshaller(TypeLiteral parameterizedType, Class... annotationTypes) { + BinaryMarshaller res = lookupBinaryMarshallerImpl(parameterizedType.getType(), annotationTypes); + if (res != null) { + return (BinaryMarshaller) res; + } else { + throw unsupported(parameterizedType.getType()); + } + } + + Function, ThreadLocal> getThreadLocalFactory() { + return threadLocalFactory; + } + + long attachThread(long isolate) { + return attachThreadAction.applyAsLong(isolate); + } + + boolean detachThread(long isolateThread) { + return detachThreadAction.applyAsLong(isolateThread) == 0; + } + + boolean releaseNativeObject(long isolateThread, long handle) { + return releaseNativeObjectAction.applyAsLong(isolateThread, handle) == 0; + } + + boolean shutDownIsolate(long isolate, long isolateThread) { + return shutDownIsolateAction.applyAsLong(isolate, isolateThread) == 0; + } + + private static RuntimeException unsupported(Type type) { + throw new UnsupportedOperationException(String.format("Marshalling of %s is not supported", type)); + } + + @SafeVarargs + private final BinaryMarshaller lookupBinaryMarshallerImpl(Type type, Class... annotationTypes) { + for (Class annotationType : annotationTypes) { + verifyAnnotation(annotationType); + BinaryMarshaller res = lookup(annotationBinaryMarshallers, type, annotationType); + if (res != null) { + return res; + } + } + return binaryMarshallers.get(type); + } + + private static T lookup(Map, List, T>>> marshallers, Type type, Class annotationType) { + List, T>> marshallersForAnnotation = marshallers.get(annotationType); + if (marshallersForAnnotation != null) { + Class rawType = erasure(type); + for (Pair, T> marshaller : marshallersForAnnotation) { + if (marshaller.getLeft().isAssignableFrom(rawType)) { + return marshaller.getRight(); + } + } + } + return null; + } + + private static Class erasure(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType parameterizedType) { + return (Class) parameterizedType.getRawType(); + } else if (type instanceof GenericArrayType genericArrayType) { + return arrayTypeFromComponentType(erasure(genericArrayType.getGenericComponentType())); + } else { + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + + private static Class arrayTypeFromComponentType(Class componentType) { + return Array.newInstance(componentType, 0).getClass(); + } + + private static void verifyAnnotation(Class annotationType) { + if (annotationType.getAnnotation(MarshallerAnnotation.class) == null) { + throw new IllegalArgumentException(String.format("The %s in not a valid marshaller annotation. The marshaller annotation must be annotated by the %s meta-annotation.", + annotationType.getSimpleName(), MarshallerAnnotation.class.getSimpleName())); + } + } + + public static Builder newBuilder() { + return new Builder(); + } + + /** + * A builder class to construct {@link JNIConfig} instances. + */ + public static final class Builder { + + private static final LongUnaryOperator ATTACH_UNSUPPORTED = (isolate) -> { + throw new UnsupportedOperationException("Attach is not supported."); + }; + private static final LongUnaryOperator DETACH_UNSUPPORTED = (isolateThread) -> { + throw new UnsupportedOperationException("Detach is not supported."); + }; + private static final LongBinaryOperator SHUTDOWN_UNSUPPORTED = (isolate, isolateThread) -> { + throw new UnsupportedOperationException("Isolate shutdown is not supported."); + }; + private static final LongBinaryOperator RELEASE_UNSUPPORTED = (isolateThread, handle) -> { + throw new UnsupportedOperationException("Native object clean up is not supported."); + }; + + private final Map> binaryMarshallers; + private final Map, List, BinaryMarshaller>>> annotationBinaryMarshallers; + private LongUnaryOperator attachThreadAction = ATTACH_UNSUPPORTED; + private LongUnaryOperator detachThreadAction = DETACH_UNSUPPORTED; + private LongBinaryOperator shutDownIsolateAction = SHUTDOWN_UNSUPPORTED; + private LongBinaryOperator releaseNativeObjectAction = RELEASE_UNSUPPORTED; + private Function, ThreadLocal> threadLocalFactory = ThreadLocal::withInitial; + + Builder() { + this.binaryMarshallers = new HashMap<>(); + this.annotationBinaryMarshallers = new HashMap<>(); + // Register default marshallers + this.binaryMarshallers.put(String.class, BinaryMarshaller.nullable(new StringMarshaller())); + this.binaryMarshallers.put(Throwable.class, new DefaultThrowableMarshaller()); + this.binaryMarshallers.put(StackTraceElement.class, StackTraceElementMarshaller.INSTANCE); + } + + /** + * Registers a {@link BinaryMarshaller} for the {@code type}. + * + * @param type the type to register {@link BinaryMarshaller} for. + * @param marshaller the marshaller to register. + */ + public Builder registerMarshaller(Class type, BinaryMarshaller marshaller) { + Objects.requireNonNull(type, "Type must be non null."); + Objects.requireNonNull(marshaller, "Marshaller must be non null."); + this.binaryMarshallers.put(type, marshaller); + return this; + } + + /** + * Registers a {@link BinaryMarshaller} for the {@code parameterizedType}. + * + * @param parameterizedType the type to register {@link BinaryMarshaller} for. + * @param marshaller the marshaller to register. + */ + public Builder registerMarshaller(TypeLiteral parameterizedType, BinaryMarshaller marshaller) { + Objects.requireNonNull(parameterizedType, "ParameterizedType must be non null."); + Objects.requireNonNull(marshaller, "Marshaller must be non null."); + this.binaryMarshallers.put(parameterizedType.getType(), marshaller); + return this; + } + + /** + * Registers a {@link BinaryMarshaller} for the {@code type} and {@code annotationType}. + * + * @param type the type to register {@link BinaryMarshaller} for. + * @param annotationType a required annotation to look up the marshaller. + * @param marshaller the marshaller to register. + * + */ + public Builder registerMarshaller(Class type, Class annotationType, BinaryMarshaller marshaller) { + Objects.requireNonNull(type, "Type must be non null."); + Objects.requireNonNull(annotationType, "AnnotationType must be non null."); + Objects.requireNonNull(marshaller, "Marshaller must be non null."); + insert(annotationBinaryMarshallers, type, annotationType, marshaller); + return this; + } + + /** + * Registers a {@link BinaryMarshaller} for the {@code parameterizedType} and + * {@code annotationType}. + * + * @param parameterizedType the type to register {@link BinaryMarshaller} for. + * @param annotationType a required annotation to look up the marshaller. + * @param marshaller the marshaller to register. + * + */ + public Builder registerMarshaller(TypeLiteral parameterizedType, Class annotationType, BinaryMarshaller marshaller) { + Objects.requireNonNull(parameterizedType, "ParameterizedType must be non null."); + Objects.requireNonNull(annotationType, "AnnotationType must be non null."); + Objects.requireNonNull(marshaller, "Marshaller must be non null."); + insert(annotationBinaryMarshallers, parameterizedType.getRawType(), annotationType, marshaller); + return this; + } + + private static void insert(Map, List, T>>> into, Class type, Class annotationType, T marshaller) { + verifyAnnotation(annotationType); + List, T>> types = into.computeIfAbsent(annotationType, (k) -> new LinkedList<>()); + Pair, T> toInsert = Pair.create(type, marshaller); + boolean inserted = false; + for (ListIterator, T>> it = types.listIterator(); it.hasNext();) { + Pair, T> current = it.next(); + if (current.getLeft().isAssignableFrom(type)) { + it.set(toInsert); + it.add(current); + inserted = true; + break; + } + } + if (!inserted) { + types.add(toInsert); + } + } + + /** + * Registers a callback used by the {@link NativeIsolate} to attach the current thread to an + * isolate. + * + * @param action a {@link LongUnaryOperator} that takes an isolate address as a parameter + * and returns the isolate thread address. + */ + public Builder setAttachThreadAction(LongUnaryOperator action) { + Objects.requireNonNull(action, "Action must be non null."); + this.attachThreadAction = action; + return this; + } + + /** + * Registers a callback used by the {@link NativeIsolate} to detach the current thread from + * an isolate. + * + * @param action a {@link LongUnaryOperator} that takes an isolate thread address as a + * parameter and returns {@code 0} on success or non-zero in case of an error. + */ + public Builder setDetachThreadAction(LongUnaryOperator action) { + Objects.requireNonNull(action, "Action must be non null."); + this.detachThreadAction = action; + return this; + } + + /** + * Registers a callback used by the {@link NativeIsolate} to tear down the isolate. + * + * @param action a {@link LongBinaryOperator} that takes an isolate address and an isolate + * thread address as parameters and returns {@code 0} on success or non-zero in + * case of an error. + */ + public Builder setShutDownIsolateAction(LongBinaryOperator action) { + Objects.requireNonNull(action, "Action must be non null."); + this.shutDownIsolateAction = action; + return this; + } + + /** + * Registers a callback used by the {@link NativeObject} to free an object in a native image + * heap referenced by the garbage-collected handle. At some point after a + * {@link NativeObject} is garbage collected, a call to the {@code action} is made to + * release the corresponding object in the native image heap. + * + * @param action a {@link LongBinaryOperator} that takes an isolate thread address and + * object handle as parameters and returns {@code 0} on success or non-zero in + * case of an error. + * + * @see NativeObject + */ + public Builder setReleaseNativeObjectAction(LongBinaryOperator action) { + Objects.requireNonNull(action, "Action must be non null."); + this.releaseNativeObjectAction = action; + return this; + } + + /** + * Registers a thread local factory whenever the default thread local handling should be + * overriden. This can be useful to install a terminating thread local using JVMCI services + * when needed. + * + * @see NativeObject + */ + public Builder setNativeThreadLocalFactory(Function, ThreadLocal> factory) { + Objects.requireNonNull(factory, "Action must be non null."); + this.threadLocalFactory = factory; + return this; + } + + /** + * Builds the {@link JNIConfig}. + */ + public JNIConfig build() { + return new JNIConfig(binaryMarshallers, annotationBinaryMarshallers, + attachThreadAction, detachThreadAction, shutDownIsolateAction, + releaseNativeObjectAction, threadLocalFactory); + } + + /** + * Returns a {@link BinaryMarshaller} for stack trace marshalling. + */ + public static BinaryMarshaller defaultStackTraceMarshaller() { + return DefaultStackTraceMarshaller.INSTANCE; + } + } + +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/MarshalledException.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/MarshalledException.java new file mode 100644 index 000000000000..92541c703c2f --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/MarshalledException.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +/** + * An exception representing an exception thrown over the isolate boundary. + */ +@SuppressWarnings("serial") +public final class MarshalledException extends RuntimeException { + + private final String foreignExceptionClassName; + + /** + * Creates a {@link MarshalledException} for foreign exception of the + * {@code foreignExceptionClassName} type with the {@code foreignExceptionMessage} message. + * + * @param foreignExceptionClassName the foreign exception class name + * @param foreignExceptionMessage the foreign exception message + * @param stackTrace the merged stack trace. + */ + public MarshalledException(String foreignExceptionClassName, String foreignExceptionMessage, StackTraceElement[] stackTrace) { + super(foreignExceptionMessage); + this.foreignExceptionClassName = foreignExceptionClassName; + setStackTrace(stackTrace); + } + + /** + * Returns the foreign exception class name. + */ + public String getForeignExceptionClassName() { + return foreignExceptionClassName; + } + + @Override + @SuppressWarnings("sync-override") + public Throwable fillInStackTrace() { + return this; + } + + @Override + public String toString() { + String message = getLocalizedMessage(); + return (message != null) ? (foreignExceptionClassName + ": " + message) : foreignExceptionClassName; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/MarshallerAnnotation.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/MarshallerAnnotation.java new file mode 100644 index 000000000000..f7ecfc99bc70 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/MarshallerAnnotation.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Meta-annotation that marks an annotation to be used for marshaller lookup. An annotation intended + * for {@link JNIConfig#lookupMarshaller(Class, Class[]) marshaller lookup} must be annotated by + * this annotation. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface MarshallerAnnotation { +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeIsolate.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeIsolate.java new file mode 100644 index 000000000000..80ef465a0676 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeIsolate.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Represents a single native image isolate. All {@link NativeObject}s have a {@link NativeIsolate} + * context. + */ +public final class NativeIsolate { + + static final int CLOSED = -1; + private static final long NULL = 0L; + private static final Map isolates = new ConcurrentHashMap<>(); + private static final AtomicInteger UUIDS = new AtomicInteger(0); + + private final long uuid; + private final long isolateId; + private final JNIConfig config; + private final ThreadLocal attachedIsolateThread; + private final Collection threads; // Guarded by this + final Set> cleaners; + private volatile State state; // Guarded by this + + private NativeIsolate(long isolateId, JNIConfig config) { + if (isolateId == NULL) { + throw new IllegalArgumentException("Isolate address must be non NULL"); + } + this.uuid = UUIDS.incrementAndGet(); + this.isolateId = isolateId; + this.config = config; + this.cleaners = Collections.newSetFromMap(new ConcurrentHashMap<>()); + this.threads = new ArrayList<>(); + this.attachedIsolateThread = config.getThreadLocalFactory().apply(() -> null); + this.state = State.ACTIVE; + } + + /** + * Binds a native image thread to this isolate. When a thread created in the native image enters + * for the first time to the host, it must be registered to the {@link NativeIsolate} as a + * native thread. + * + * @param isolateThreadId the isolate thread to bind. + */ + public void registerNativeThread(long isolateThreadId) { + NativeIsolateThread nativeIsolateThread = attachedIsolateThread.get(); + if (nativeIsolateThread != null) { + throw new IllegalStateException(String.format("Native thread %s is already attached to isolate %s.", Thread.currentThread(), this)); + } + synchronized (this) { + if (!state.isValid()) { + throw throwClosedException(); + } + nativeIsolateThread = new NativeIsolateThread(Thread.currentThread(), this, true, isolateThreadId); + threads.add(nativeIsolateThread); + attachedIsolateThread.set(nativeIsolateThread); + } + } + + /** + * Enters this {@link NativeIsolate} on the current thread. + * + * @throws IllegalStateException when this {@link NativeObject} is already closed or being + * closed. + */ + public NativeIsolateThread enter() { + NativeIsolateThread nativeIsolateThread = getOrCreateNativeIsolateThread(); + if (nativeIsolateThread != null && nativeIsolateThread.enter()) { + return nativeIsolateThread; + } else { + throw throwClosedException(); + } + } + + /** + * Tries to enter this {@link NativeIsolate} on the current thread. + * + * @return {@link NativeIsolateThread} on success or {@code null} when this + * {@link NativeIsolate} is closed or being closed. + * @see #enter() + */ + public NativeIsolateThread tryEnter() { + NativeIsolateThread nativeIsolateThread = getOrCreateNativeIsolateThread(); + if (nativeIsolateThread != null && nativeIsolateThread.enter()) { + return nativeIsolateThread; + } else { + return null; + } + } + + /** + * Returns true if the current thread is entered to this {@link NativeIsolate}. + */ + public boolean isActive() { + NativeIsolateThread nativeIsolateThread = attachedIsolateThread.get(); + return nativeIsolateThread != null && (nativeIsolateThread.isNativeThread() || nativeIsolateThread.isActive()); + } + + /** + * Requests an isolate shutdown. If there is no host thread entered into this + * {@link NativeIsolate} the isolate is closed and the isolate heap is freed. If this + * {@link NativeIsolate} has active threads the isolate is freed by the last leaving thread. + */ + public boolean shutdown() { + NativeIsolateThread currentIsolateThread = attachedIsolateThread.get(); + if (currentIsolateThread != null && currentIsolateThread.isNativeThread()) { + return false; + } + boolean deferredClose = false; + synchronized (this) { + if (state == State.DISPOSED) { + return true; + } + state = State.DISPOSING; + for (NativeIsolateThread nativeIsolateThread : threads) { + deferredClose |= !nativeIsolateThread.invalidate(); + } + } + if (deferredClose) { + return false; + } else { + return doIsolateShutdown(); + } + } + + /** + * Returns the isolate address. + */ + public long getIsolateId() { + return isolateId; + } + + /** + * Returns the {@link JNIConfig} used by this {@link NativeIsolate}. + */ + public JNIConfig getConfig() { + return config; + } + + @Override + public String toString() { + return "NativeIsolate[" + uuid + " for 0x" + Long.toHexString(isolateId) + "]"; + } + + /** + * Gets the NativeIsolate object for the entered isolate with the specified isolate address. + * IMPORTANT: Must be used only when the isolate with the specified isolateId is entered. + * + * @param isolateId id of an entered isolate + * @return NativeIsolate object for the entered isolate with the specified isolate address + * @throws IllegalStateException when {@link NativeIsolate} does not exist for the + * {@code isolateId} + */ + public static NativeIsolate get(long isolateId) { + NativeIsolate res = isolates.get(isolateId); + if (res == null) { + throw new IllegalStateException("NativeIsolate for isolate 0x" + Long.toHexString(isolateId) + " does not exist."); + } + return res; + } + + /** + * Creates a {@link NativeIsolate} for the {@code isolateId} and {@link JNIConfig}. This method + * can be called at most once, preferably right after creating the isolate. Use the + * {@link #get(long)} method to get an existing {@link NativeIsolate} instance. + * + * @return the newly created {@link NativeIsolate} for the {@code isolateId}. + * @throws IllegalStateException when {@link NativeIsolate} for the {@code isolateId} already + * exists. + */ + public static NativeIsolate forIsolateId(long isolateId, JNIConfig config) { + NativeIsolate res = new NativeIsolate(isolateId, config); + NativeIsolate previous = isolates.put(isolateId, res); + if (previous != null && previous.state != State.DISPOSED) { + throw new IllegalStateException("NativeIsolate for isolate 0x" + Long.toHexString(isolateId) + " already exists and is not disposed."); + } + return res; + } + + /* + * Returns true if the isolate shutdown process has already begun or is finished. + */ + public boolean isDisposed() { + return state == State.DISPOSED; + } + + void lastLeave() { + synchronized (this) { + for (NativeIsolateThread nativeIsolateThread : threads) { + if (nativeIsolateThread.isActive()) { + return; + } + } + } + doIsolateShutdown(); + } + + RuntimeException throwClosedException() { + throw new IllegalStateException("Isolate 0x" + Long.toHexString(getIsolateId()) + " is already closed."); + } + + private boolean doIsolateShutdown() { + synchronized (this) { + if (state == State.DISPOSED) { + return true; + } + state = State.DISPOSED; + } + cleaners.clear(); + boolean success = false; + + NativeIsolateThread nativeIsolateThread = attachedIsolateThread.get(); + if (nativeIsolateThread == null) { + nativeIsolateThread = new NativeIsolateThread(Thread.currentThread(), this, false, config.attachThread(isolateId)); + nativeIsolateThread.invalidate(); + attachedIsolateThread.set(nativeIsolateThread); + } + try { + nativeIsolateThread.setShutDownRequest(true); + try { + success = config.shutDownIsolate(isolateId, nativeIsolateThread.isolateThread); + } finally { + nativeIsolateThread.setShutDownRequest(false); + } + } finally { + if (success) { + isolates.computeIfPresent(isolateId, (id, nativeIsolate) -> (nativeIsolate == NativeIsolate.this ? null : nativeIsolate)); + } + } + return success; + } + + private NativeIsolateThread getOrCreateNativeIsolateThread() { + NativeIsolateThread nativeIsolateThread = attachedIsolateThread.get(); + if (nativeIsolateThread == null) { + synchronized (this) { + if (!state.isValid()) { + return null; + } + long isolateThreadAddress = config.attachThread(isolateId); + nativeIsolateThread = new NativeIsolateThread(Thread.currentThread(), this, false, isolateThreadAddress); + threads.add(nativeIsolateThread); + attachedIsolateThread.set(nativeIsolateThread); + } + } + return nativeIsolateThread; + } + + public void detachCurrentThread() { + synchronized (this) { + NativeIsolateThread isolateThread = attachedIsolateThread.get(); + if (isolateThread != null) { + detachThread(isolateThread); + attachedIsolateThread.set(null); + } + } + } + + private synchronized void detachThread(NativeIsolateThread nativeIsolateThread) { + if (state.isValid() && nativeIsolateThread != null && !nativeIsolateThread.isNativeThread()) { + config.detachThread(nativeIsolateThread.isolateThread); + } + } + + private enum State { + + ACTIVE, + DISPOSING, + DISPOSED; + + boolean isValid() { + return this == ACTIVE; + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeIsolateThread.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeIsolateThread.java new file mode 100644 index 000000000000..f6dafb2618df --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeIsolateThread.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import static com.oracle.svm.jdwp.bridge.nativebridge.NativeIsolate.CLOSED; + +import java.lang.ref.WeakReference; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Represents an entered isolate thread. + * + * @see NativeIsolate#enter() + */ +public final class NativeIsolateThread { + + private static final int CLOSING_MASK = 0b1; + + private final NativeIsolate isolate; + private final WeakReference thread; + private final AtomicInteger enteredCount; + private final boolean nativeThread; + final long isolateThread; + private boolean executesShutDown; + + NativeIsolateThread(Thread thread, NativeIsolate isolate, boolean nativeThread, long isolateThreadId) { + this.thread = new WeakReference<>(thread); + this.isolate = isolate; + this.nativeThread = nativeThread; + this.isolateThread = isolateThreadId; + this.enteredCount = new AtomicInteger(); + } + + /** + * Returns the isolate thread address. + * + * @throws IllegalStateException when the {@link NativeIsolateThread} is no more entered. + */ + public long getIsolateThreadId() { + assert verifyThread(); + if (!isActive()) { + throw new IllegalStateException("Isolate 0x" + Long.toHexString(isolate.getIsolateId()) + " is not entered."); + } + return isolateThread; + } + + /** + * Returns the isolate for this thread. + */ + public NativeIsolate getIsolate() { + return isolate; + } + + /** + * Leaves the {@link NativeIsolate} on the current thread. + */ + public void leave() { + assert verifyThread(); + decrementAttached(); + } + + boolean enter() { + assert verifyThread(); + return incrementAttached(); + } + + boolean invalidate() { + while (true) { // TERMINATION ARGUMENT: busy loop + int value = enteredCount.get(); + if (value == CLOSED) { + return true; + } + int numberOfAttached = (value >>> 1); + boolean inactive = numberOfAttached == 0; + int newValue = inactive ? CLOSED : (value | CLOSING_MASK); + if (enteredCount.compareAndSet(value, newValue)) { + return inactive; + } + } + } + + boolean isActive() { + if (executesShutDown) { + return true; + } + int value = enteredCount.get(); + return value != CLOSED && (value >>> 1) > 0; + } + + boolean isNativeThread() { + return nativeThread; + } + + void setShutDownRequest(boolean shutDown) { + this.executesShutDown = shutDown; + } + + private boolean verifyThread() { + assert thread.get() == Thread.currentThread() : String.format( + "NativeIsolateThread used by other thread. Expected thread %s, actual thread %s.", + thread.get(), + Thread.currentThread()); + return true; + } + + private boolean incrementAttached() { + while (true) { // TERMINATION ARGUMENT: busy loop + int value = enteredCount.get(); + if (value == CLOSED) { + if (executesShutDown) { + return true; + } else { + return false; + } + } + int closing = (value & CLOSING_MASK); + int newValue = (((value >>> 1) + 1) << 1) | closing; + if (enteredCount.compareAndSet(value, newValue)) { + break; + } + } + return true; + } + + private void decrementAttached() { + while (true) { // TERMINATION ARGUMENT: busy loop + int value = enteredCount.get(); + if (value == CLOSED) { + if (executesShutDown) { + return; + } else { + throw new IllegalStateException("Isolate 0x" + Long.toHexString(isolate.getIsolateId()) + " was closed while being active."); + } + } + int closing = (value & CLOSING_MASK); + int numberOfAttached = (value >>> 1) - 1; + boolean lastLeaving = closing == CLOSING_MASK && numberOfAttached == 0; + int newValue = lastLeaving ? CLOSED : ((numberOfAttached << 1) | closing); + if (enteredCount.compareAndSet(value, newValue)) { + if (lastLeaving) { + isolate.lastLeave(); + } + break; + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObject.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObject.java new file mode 100644 index 000000000000..a3193a357775 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObject.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +/** + * Encapsulates a handle to an object in a native image heap where the object's lifetime is bound to + * the lifetime of the {@link NativeObject} instance. At some point, after a {@link NativeObject} is + * garbage collected, a call is made to release the handle, allowing the corresponding object in the + * native image heap to be collected. + */ +public class NativeObject { + + private final NativeIsolate isolate; + private final long objectHandle; + private final NativeObjectCleaner cleanup; + + /** + * Creates a new {@link NativeObject}. + * + * @param isolate an isolate in which an object referenced by the handle exists. + * @param objectHandle a handle to an object in a native image heap + */ + public NativeObject(NativeIsolate isolate, long objectHandle) { + this.isolate = isolate; + this.objectHandle = objectHandle; + this.cleanup = new NativeObjectCleanerImpl(this).register(); + } + + /** + * Returns an isolate in which an object referenced by this handle exists. + */ + public final NativeIsolate getIsolate() { + return isolate; + } + + /** + * Returns a handle to an object in the native image heap. + */ + public final long getHandle() { + return objectHandle; + } + + /** + * Explicitly releases object in the native image heap referenced by this handle. The use of + * this method should be exceptional. By default, the lifetime of the object in the native image + * heap is bound to the lifetime of the {@link NativeObject} instance. + */ + public final void release() { + if (isolate.cleaners.remove(cleanup)) { + NativeIsolateThread nativeIsolateThread = isolate.enter(); + try { + cleanup.cleanUp(nativeIsolateThread.getIsolateThreadId()); + } finally { + nativeIsolateThread.leave(); + } + } + } + + private static final class NativeObjectCleanerImpl extends NativeObjectCleaner { + + private final long handle; + + NativeObjectCleanerImpl(NativeObject nativeObject) { + super(nativeObject, nativeObject.getIsolate()); + this.handle = nativeObject.getHandle(); + } + + @Override + public void cleanUp(long isolateThread) { + isolate.getConfig().releaseNativeObject(isolateThread, handle); + } + + @Override + public String toString() { + return "NativeObject 0x" + Long.toHexString(handle); + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObjectCleaner.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObjectCleaner.java new file mode 100644 index 000000000000..ac3bc9a0ff2d --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObjectCleaner.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +/** + * A weak reference performing a given action when a referent becomes weakly reachable and is + * enqueued into reference queue. The class is not active. The reference queue check is performed + * anytime a new {@link NativeObjectCleaner#register()} is called, or it can be performed explicitly + * using {@link NativeObjectCleaner#processPendingCleaners()}. + */ +public abstract class NativeObjectCleaner extends WeakReference { + + private static final Object INVALID_ISOLATE_THREAD = new Object(); + private static final ReferenceQueue cleanersQueue = new ReferenceQueue<>(); + + final NativeIsolate isolate; + + /** + * Creates a new {@link NativeObjectCleaner}. + * + * @param referent object the new weak reference will refer to + * @param isolate the native object isolate + */ + protected NativeObjectCleaner(T referent, NativeIsolate isolate) { + super(referent, cleanersQueue); + this.isolate = isolate; + } + + /** + * Registers {@link NativeObjectCleaner} for cleanup. + */ + public final NativeObjectCleaner register() { + processPendingCleaners(); + if (!isolate.isDisposed()) { + isolate.cleaners.add(this); + } + return this; + } + + /** + * At some point after a {@code referent} is garbage collected the {@link NativeIsolate} is + * entered and the {@link #cleanUp(long)} is executed with the isolate thread address parameter. + * This method should perform cleanup in the isolate heap. + * + * @param isolateThread the isolate thread address to call into isolate. + */ + protected abstract void cleanUp(long isolateThread); + + /** + * Performs an explicit clean up of enqueued {@link NativeObjectCleaner}s. + */ + public static void processPendingCleaners() { + Map enteredThreads = null; + NativeObjectCleaner cleaner; + try { + while ((cleaner = (NativeObjectCleaner) cleanersQueue.poll()) != null) { + NativeIsolate isolate = cleaner.isolate; + if (isolate.cleaners.remove(cleaner)) { + Object enteredThread; + if (enteredThreads == null) { + enteredThreads = new HashMap<>(); + enteredThread = null; + } else { + enteredThread = enteredThreads.get(isolate); + } + if (enteredThread == null) { + enteredThread = isolate.tryEnter(); + if (enteredThread == null) { + enteredThread = INVALID_ISOLATE_THREAD; + } + enteredThreads.put(isolate, enteredThread); + } + if (enteredThread != INVALID_ISOLATE_THREAD) { + cleanImpl(((NativeIsolateThread) enteredThread).getIsolateThreadId(), cleaner); + } + } + } + } finally { + if (enteredThreads != null) { + for (Object enteredThread : enteredThreads.values()) { + if (enteredThread != INVALID_ISOLATE_THREAD) { + ((NativeIsolateThread) enteredThread).leave(); + } + } + } + } + } + + private static void cleanImpl(long isolateThread, NativeObjectCleaner cleaner) { + cleaner.cleanUp(isolateThread); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObjectHandles.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObjectHandles.java new file mode 100644 index 000000000000..e1c623aeea73 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NativeObjectHandles.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import org.graalvm.nativeimage.ObjectHandles; +import org.graalvm.word.WordFactory; + +/** + * A support class for mapping objects in the native image isolate to long handles. + */ +public final class NativeObjectHandles { + + private NativeObjectHandles() { + } + + /** + * Creates a handle for the {@code object}. Unless the handle is {@link #remove(long) removed} + * the {@code object} stays strongly reachable. + */ + public static long create(Object object) { + return ObjectHandles.getGlobal().create(object).rawValue(); + } + + /** + * Resolves a handle into an object. + * + * @throws InvalidHandleException if the handle is either already removed or invalid. + */ + public static T resolve(long handle, Class type) { + try { + return type.cast(ObjectHandles.getGlobal().get(WordFactory.pointer(handle))); + } catch (IllegalArgumentException iae) { + throw new InvalidHandleException(iae); + } + } + + /** + * Removes a handle. Allows an object identified by the {@code handle} to be garbage collected. + */ + public static void remove(long handle) { + try { + ObjectHandles.getGlobal().destroy(WordFactory.pointer(handle)); + } catch (IllegalArgumentException iae) { + throw new InvalidHandleException(iae); + } + } + + /** + * An exception thrown when an invalid handle is resolved. + * + * @see #resolve(long, Class) + */ + public static final class InvalidHandleException extends IllegalArgumentException { + + private static final long serialVersionUID = 1L; + + InvalidHandleException(IllegalArgumentException cause) { + super(cause.getMessage(), cause); + setStackTrace(cause.getStackTrace()); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NullableBinaryMarshaller.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NullableBinaryMarshaller.java new file mode 100644 index 000000000000..2c51df574778 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/NullableBinaryMarshaller.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +final class NullableBinaryMarshaller implements BinaryMarshaller { + + private static final byte NULL = 0; + private static final byte NON_NULL = 1; + + private final BinaryMarshaller delegate; + + NullableBinaryMarshaller(BinaryMarshaller delegate) { + this.delegate = delegate; + } + + @Override + public T read(BinaryInput input) { + byte nullStatus = input.readByte(); + switch (nullStatus) { + case NULL: + return null; + case NON_NULL: + return delegate.read(input); + default: + throw new IllegalArgumentException("Unexpected input " + nullStatus); + } + } + + @Override + public void write(BinaryOutput output, T object) { + if (object != null) { + output.writeByte(NON_NULL); + delegate.write(output, object); + } else { + output.writeByte(NULL); + } + } + + @Override + public void readUpdate(BinaryInput input, T object) { + byte nullStatus = input.readByte(); + switch (nullStatus) { + case NULL: + assert object == null; + break; + case NON_NULL: + assert object != null; + delegate.readUpdate(input, object); + break; + default: + throw new IllegalArgumentException("Unexpected input " + nullStatus); + } + } + + @Override + public void writeUpdate(BinaryOutput output, T object) { + if (object != null) { + output.writeByte(NON_NULL); + delegate.writeUpdate(output, object); + } else { + output.writeByte(NULL); + } + } + + @Override + public int inferSize(T object) { + if (object != null) { + return 1 + delegate.inferSize(object); + } else { + return 1; + } + } + + @Override + public int inferUpdateSize(T object) { + if (object != null) { + return 1 + delegate.inferUpdateSize(object); + } else { + return 1; + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/Out.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/Out.java new file mode 100644 index 000000000000..055c0d68dd12 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/Out.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Configures an array parameter as an out-parameter. For an out-parameter, the array value is + * copied over the boundary from a called method. It may be combined with {@link In} for in-out + * parameters. Example showing the configuration for + * {@link java.io.OutputStream#write(byte[], int, int)}. + * + *
+ * @Override
+ * public abstract int read(@Out(arrayOffsetParameter = "off", arrayLengthParameter = "len", trimToResult = true) byte[] b, int off, int len);
+ * 
+ */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.PARAMETER) +public @interface Out { + + /** + * Copy only a part of the array starting at offset given by the {@code arrayOffsetParameter} + * method parameter. By default, the whole array is copied. The {@code arrayOffsetParameter} can + * be used to improve the performance and copy only a part of the array over the boundary. + */ + String arrayOffsetParameter() default ""; + + /** + * Limits copying only to many of the elements given by the {@code arrayLengthParameter} + * parameter. By default, the whole array is copied. The {@code arrayLengthParameter} can be + * used to improve the performance and copy only a part of the array over the boundary. + */ + String arrayLengthParameter() default ""; + + /** + * Limits copying only to method result number of elements. It can be used to further limit the + * number of copied elements in addition to {@link #arrayLengthParameter}. When used, it's still + * good to specify {@link #arrayLengthParameter} as an upper bound to limit allocated array + * size. + */ + boolean trimToResult() default false; +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/StackTraceElementMarshaller.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/StackTraceElementMarshaller.java new file mode 100644 index 000000000000..691a9871e506 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/StackTraceElementMarshaller.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +final class StackTraceElementMarshaller implements BinaryMarshaller { + + static final StackTraceElementMarshaller INSTANCE = new StackTraceElementMarshaller(); + + private StackTraceElementMarshaller() { + } + + private static final int STACK_TRACE_ELEMENT_SIZE_ESTIMATE = 100; + + @Override + public StackTraceElement read(BinaryInput in) { + String className = in.readUTF(); + String methodName = in.readUTF(); + String fileName = in.readUTF(); + fileName = fileName.isEmpty() ? null : fileName; + int lineNumber = in.readInt(); + return new StackTraceElement(className, methodName, fileName, lineNumber); + } + + @Override + public void write(BinaryOutput out, StackTraceElement stackTraceElement) { + out.writeUTF(stackTraceElement.getClassName()); + out.writeUTF(stackTraceElement.getMethodName()); + String fileName = stackTraceElement.getFileName(); + out.writeUTF(fileName == null ? "" : fileName); + out.writeInt(stackTraceElement.getLineNumber()); + } + + @Override + public int inferSize(StackTraceElement object) { + return STACK_TRACE_ELEMENT_SIZE_ESTIMATE; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/StringMarshaller.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/StringMarshaller.java new file mode 100644 index 000000000000..acf2976d56b8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/StringMarshaller.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +/** + * BinaryMarshaller used to marshall string array components. + */ +final class StringMarshaller implements BinaryMarshaller { + + private static final int STRING_SIZE_ESTIMATE = 32; + + @Override + public String read(BinaryInput in) { + return in.readUTF(); + } + + @Override + public void write(BinaryOutput out, String str) { + out.writeUTF(str); + } + + @Override + public int inferSize(String object) { + return STRING_SIZE_ESTIMATE; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/TypeLiteral.java b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/TypeLiteral.java new file mode 100644 index 000000000000..6a32a6399526 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.bridge/src/com/oracle/svm/jdwp/bridge/nativebridge/TypeLiteral.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.bridge.nativebridge; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * Represents a generic type {@code T}. Java doesn't yet provide a way to represent generic types, + * so this class does. Forces clients to create a subclass of this class which enables retrieval of + * the type information even at runtime. + * + *

+ * For example, to create a type literal for {@code List}, you can create an empty anonymous + * inner class: + * + *

+ * {@code TypeLiteral> list = new TypeLiteral>() {};} + * + */ +public abstract class TypeLiteral { + + private final Type type; + private final Class rawType; + + /** + * Constructs a new type literal. Derives represented class from type parameter. + * + *

+ * Clients create an empty anonymous subclass. Doing so embeds the type parameter in the + * anonymous class's type hierarchy so we can reconstitute it at runtime despite erasure. + * + * @since 19.0 + */ + @SuppressWarnings("unchecked") + protected TypeLiteral() { + this.type = extractLiteralType(this.getClass()); + this.rawType = (Class) extractRawType(type); + } + + private static Type extractLiteralType(@SuppressWarnings("rawtypes") Class literalClass) { + Type superType = literalClass.getGenericSuperclass(); + Type typeArgument = null; + while (true) { + if (superType instanceof ParameterizedType) { + ParameterizedType parametrizedType = (ParameterizedType) superType; + if (parametrizedType.getRawType() == TypeLiteral.class) { + // found + typeArgument = parametrizedType.getActualTypeArguments()[0]; + break; + } else { + throw new AssertionError("Unsupported type hierarchy for type literal."); + } + } else if (superType instanceof Class) { + if (superType == TypeLiteral.class) { + typeArgument = Object.class; + break; + } else { + superType = ((Class) superType).getGenericSuperclass(); + } + } else { + throw new AssertionError("Unsupported type hierarchy for type literal."); + } + } + return typeArgument; + } + + private static Class extractRawType(Type type) { + Class rawType; + if (type instanceof Class) { + rawType = (Class) type; + } else if (type instanceof ParameterizedType) { + rawType = (Class) ((ParameterizedType) type).getRawType(); + } else if (type instanceof GenericArrayType) { + rawType = arrayTypeFromComponentType(extractRawType(((GenericArrayType) type).getGenericComponentType())); + } else { + throw new IllegalArgumentException("Unsupported type: " + type); + } + return rawType; + } + + private static Class arrayTypeFromComponentType(Class componentType) { + return Array.newInstance(componentType, 0).getClass(); + } + + /** + * Returns the type literal including generic type information. + */ + public final Type getType() { + return this.type; + } + + /** + * Returns the raw class type of the literal. + */ + public final Class getRawType() { + return rawType; + } + + @Override + public final boolean equals(Object obj) { + return super.equals(obj); + } + + @Override + public final int hashCode() { + return super.hashCode(); + } + + @Override + public final String toString() { + return "TypeLiteral<" + type + ">"; + } + +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ClassUtils.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ClassUtils.java new file mode 100644 index 000000000000..ca7e2e1b658c --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ClassUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.interpreter.DebuggerSupport; +import com.oracle.svm.jdwp.bridge.ClassStatusConstants; + +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * Utility methods for ResolvedJavaType. + */ +public final class ClassUtils { + + private ClassUtils() { + throw new UnsupportedOperationException("Do not instantiate"); + } + + /** + * Returns a String representation of the name of the class. + * + * @return the name of the class + */ + public static String getNameAsString(ResolvedJavaType type) { + return type.toClassName(); + } + + /** + * Returns the String representation of the type of the class. + * + * @return the class type descriptor name + */ + public static String getTypeAsString(ResolvedJavaType type) { + return type.getName(); // toJavaName(); + } + + /** + * Returns the String representation of the generic type of the class. + * + * @return the generic class type descriptor name + */ + public static String getGenericTypeAsString(@SuppressWarnings("unused") ResolvedJavaType type) { + // TODO(peterssen): GR-55068 The JDWP debugger should provide generic type names and + // signatures. + // An empty string means no information available. + return ""; + } + + /** + * @return the status according to ClassStatusConstants of the class + */ + public static int getStatus(@SuppressWarnings("unused") ResolvedJavaType type) { + Class clazz = DebuggerSupport.singleton().getUniverse().lookupClass(type); + + // Classes are always prepared and verified under closed-world assumptions. + int status = ClassStatusConstants.PREPARED | ClassStatusConstants.VERIFIED; + + DynamicHub hub = DynamicHub.fromClass(clazz); + if (hub.isLoaded()) { + // Just for completeness since ClassStatusConstants.LOADED is 0. + status |= ClassStatusConstants.LOADED; + } + if (hub.isInitialized()) { + status |= ClassStatusConstants.INITIALIZED; + } + if (hub.getClassInitializationInfo().isInErrorState()) { + status |= ClassStatusConstants.ERROR; + } + + return status; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/CopyNativeJDWPLibraryFeature.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/CopyNativeJDWPLibraryFeature.java new file mode 100644 index 000000000000..a7e381f11b0c --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/CopyNativeJDWPLibraryFeature.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import jdk.graal.compiler.core.common.SuppressFBWarnings; +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionType; + +import com.oracle.svm.core.BuildArtifacts; +import com.oracle.svm.core.OS; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.option.HostedOptionKey; + +/** + * Copies {@code lib:svmjdwp} from the GraalVM native libraries, if it exists, to the native-image + * destination directory. + * + *

+ * This is an optional feature to improve usability; so that native images compiled with + * {@link JDWPOptions#JDWP} support are ready to be debugged right out-of-the-box. + * + *

+ * Disable with {@link Options#CopyNativeJDWPLibrary -H:-CopyNativeJDWPLibrary} to build + * {@code lib:svmjdwp} with JDWP support, otherwise it gets overwritten. + */ +@AutomaticallyRegisteredFeature +final class CopyNativeJDWPLibraryFeature implements InternalFeature { + + public static class Options { + @Option(help = "Copy lib:svmjdwp, if available, to the native image destination directory", type = OptionType.Expert)// + public static final HostedOptionKey CopyNativeJDWPLibrary = new HostedOptionKey<>(true); + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return JDWPOptions.JDWP.getValue() && Options.CopyNativeJDWPLibrary.getValue(); + } + + @Override + @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "imagePath.getParent() is never null") + public void afterImageWrite(AfterImageWriteAccess access) { + Path javaHome = Paths.get(System.getProperty("java.home")); + String svmjdwpLibraryName = System.mapLibraryName("svmjdwp"); + Path svmjdwpLibraryPath = javaHome + .resolve(OS.WINDOWS.isCurrent() ? "bin" : "lib") + .resolve(svmjdwpLibraryName); + + if (Files.isRegularFile(svmjdwpLibraryPath)) { + Path imagePath = access.getImagePath(); + Path targetPath = imagePath.getParent().resolve(svmjdwpLibraryName); + try { + Files.copy(svmjdwpLibraryPath, targetPath, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + BuildArtifacts.singleton().add(BuildArtifacts.ArtifactType.SHARED_LIBRARY, targetPath); + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/DebuggingOnDemandHandler.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/DebuggingOnDemandHandler.java new file mode 100644 index 000000000000..b15b8f28f312 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/DebuggingOnDemandHandler.java @@ -0,0 +1,556 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import com.oracle.svm.core.threadlocal.FastThreadLocalWord; +import com.oracle.svm.interpreter.DebuggerSupport; +import com.oracle.svm.jdwp.bridge.Logger; +import com.oracle.svm.jdwp.bridge.nativebridge.NativeObjectHandles; +import com.oracle.svm.jdwp.resident.impl.ResidentJDWP; +import jdk.graal.compiler.core.common.SuppressFBWarnings; +import org.graalvm.nativeimage.CurrentIsolate; +import org.graalvm.nativeimage.Isolate; +import org.graalvm.nativeimage.IsolateThread; +import org.graalvm.nativeimage.ProcessProperties; +import org.graalvm.nativeimage.StackValue; +import org.graalvm.nativeimage.c.function.CEntryPoint; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.nativeimage.c.function.InvokeCFunctionPointer; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.PointerBase; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.OS; +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.c.function.CEntryPointOptions; +import com.oracle.svm.core.headers.LibC; +import com.oracle.svm.core.jdk.PlatformNativeLibrarySupport; +import com.oracle.svm.core.jni.headers.JNIEnvironment; +import com.oracle.svm.core.jni.headers.JNIEnvironmentPointer; +import com.oracle.svm.core.jni.headers.JNIErrors; +import com.oracle.svm.core.jni.headers.JNIJavaVMInitArgs; +import com.oracle.svm.core.jni.headers.JNIJavaVMOption; +import com.oracle.svm.core.jni.headers.JNIJavaVMPointer; +import com.oracle.svm.core.jni.headers.JNIMethodId; +import com.oracle.svm.core.jni.headers.JNIObjectHandle; +import com.oracle.svm.core.jni.headers.JNIVersion; +import com.oracle.svm.core.log.Log; +import com.oracle.svm.core.threadlocal.FastThreadLocalFactory; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.Interpreter; +import com.oracle.svm.interpreter.InterpreterOptions; +import com.oracle.svm.jdwp.bridge.jniutils.JNI; +import com.oracle.svm.jdwp.bridge.jniutils.JNIMethodScope; +import com.oracle.svm.jdwp.bridge.ArgFilesOption; +import com.oracle.svm.jdwp.bridge.DebugOptions.Options; +import com.oracle.svm.jdwp.bridge.JDWPEventHandlerBridge; +import com.oracle.svm.jdwp.bridge.NativeToHSJDWPEventHandlerBridge; +import com.oracle.svm.jdwp.bridge.ResidentJDWPFeatureEnabled; + +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.internal.misc.Signal; + +public class DebuggingOnDemandHandler implements Signal.Handler { + + private final Options options; + private static volatile Thread suspendThread; + + private static final int MAX_DEBUGGER_VM_OPTIONS = 32; + + public DebuggingOnDemandHandler(Options options) { + this.options = options; + } + + boolean first = true; + + private static JNI.JavaVM debuggerServerJavaVM; // Java VM running the debugger server + + private static final FastThreadLocalWord jniEnvPerThread = FastThreadLocalFactory.createWord("jniEnvPerThread"); + + public static JNI.JNIEnv currentThreadJniEnv() { + JNI.JNIEnv currentJniEnv = jniEnvPerThread.get(); + if (currentJniEnv.isNull()) { + JNI.JNIEnvPointer attachedJniEnvPointer = StackValue.get(JNI.JNIEnvPointer.class); + // Enter the debugger server (HotSpot or isolate) as a daemon thread. + debuggerServerJavaVM.getFunctions().getAttachCurrentThreadAsDaemon().call(debuggerServerJavaVM, attachedJniEnvPointer, WordFactory.nullPointer()); + currentJniEnv = attachedJniEnvPointer.readJNIEnv(); + jniEnvPerThread.set(currentJniEnv); + } + return currentJniEnv; + } + + @Override + public void handle(Signal sig) { + if (!first) { + ResidentJDWP.LOGGER.log("[DebuggingOnDemand] received USR2 signal, but JDWP server already spawned?"); + return; + } + first = false; + ResidentJDWP.LOGGER.log("[DebuggingOnDemand] received USR2 signal, spawning JDWP server"); + spawnJDWPThread(); + } + + private static class JDWPServerThread extends Thread { + private final Path libraryPath; + private final long initialThreadId; + private final Options options; + + JDWPServerThread(Path libraryPath, long initialThreadId, Options options) { + super("jdwp-server"); + this.libraryPath = libraryPath; + this.initialThreadId = initialThreadId; + this.options = options; + } + + @Override + public void run() { + DebuggingOnDemandHandler.spawnJDWPServer(libraryPath, initialThreadId, options); + ThreadStartDeathSupport.get().setDebuggerThreadServer(null); + } + } + + @SuppressFBWarnings(value = {"UW_UNCOND_WAIT", "WA_NOT_IN_LOOP"}, justification = "Intentional.") + public void spawnJDWPThread() { + long initialThreadId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(Thread.currentThread()); + + // Try to find native library before spawning the JDWP server thread, to fail early in the + // case it cannot be found. + Path libraryPath = DebuggingOnDemandHandler.findLibraryPath(options); + assert Files.isRegularFile(libraryPath); + + Thread jdwpServerThread = new JDWPServerThread(libraryPath, initialThreadId, options); + ThreadStartDeathSupport.get().setDebuggerThreadServer(jdwpServerThread); + jdwpServerThread.setDaemon(true); + jdwpServerThread.start(); + if (options.suspend()) { + suspendThread = Thread.currentThread(); + synchronized (DebuggingOnDemandHandler.class) { + try { + DebuggingOnDemandHandler.class.wait(); + } catch (InterruptedException e) { + ResidentJDWP.LOGGER.log("[DebuggingOnDemand] Interrupted initial suspend."); + } + } + } + } + + public interface JNICreateJavaVMPointer extends CFunctionPointer { + @InvokeCFunctionPointer + int call(JNIJavaVMPointer jvmptr, JNIEnvironmentPointer env, JNIJavaVMInitArgs args); + } + + public interface CallStaticVoidMethodFunctionPointer extends CFunctionPointer { + @InvokeCFunctionPointer + void invoke(JNIEnvironment env, JNIObjectHandle objOrClass, JNIMethodId methodId); + } + + public interface CallStaticObjectMethodFunctionPointer extends CFunctionPointer { + @InvokeCFunctionPointer + JNI.JObject invoke(JNIEnvironment env, JNIObjectHandle objOrClass, JNIMethodId methodId); + } + + @SuppressWarnings("try") + public static void spawnJDWPServer(Path libraryPath, long initialThreadId, Options jdwpOptions) { + assert libraryPath != null; + ResidentJDWP.LOGGER = new Logger(jdwpOptions.tracing(), "[ResidentJDWP]", System.err); + + String debuggerArgs = jdwpOptions.vmOptions(); + List vmOptions = new ArrayList<>(); + if (debuggerArgs != null && !debuggerArgs.isBlank()) { + ArgFilesOption parser = new ArgFilesOption(); + for (String arg : debuggerArgs.split("(\\R|\\s)+")) { + vmOptions.addAll(parser.process(arg)); + } + } else { + if ("jvm".equals(jdwpOptions.mode())) { + throw new IllegalArgumentException( + "The JDWP 'vm.options' option is not set or empty. It must contain at least the -Djava.class.path for the debugger, note that it also supports @argFiles arguments."); + } + } + + if (vmOptions.size() > MAX_DEBUGGER_VM_OPTIONS) { + throw VMError.shouldNotReachHere("'vm.options' contains more than " + MAX_DEBUGGER_VM_OPTIONS + " elements (including @argFile expansion)"); + } + + try (CTypeConversion.CCharPointerHolder declaringClass = CTypeConversion.toCString("com/oracle/svm/jdwp/server/JDWPServer"); + CTypeConversion.CCharPointerHolder hsMethodName = CTypeConversion.toCString("createInstance"); + CTypeConversion.CCharPointerHolder hsSignature = CTypeConversion.toCString("()Lcom/oracle/svm/jdwp/bridge/JDWPEventHandlerBridge;"); + CTypeConversion.CCharPointerPointerHolder hsVMOptions = CTypeConversion.toCStrings(vmOptions.toArray(new String[0]))) { + + ResidentJDWP.LOGGER.log("Loading libraryPath=" + libraryPath); + ResidentJDWP.LOGGER.log("jdwpOptions=" + jdwpOptions.jdwpOptions()); + ResidentJDWP.LOGGER.log("additionalOptions=" + jdwpOptions.additionalOptions()); + + JNIJavaVMInitArgs args = StackValue.get(JNIJavaVMInitArgs.class); + + JNIJavaVMOption options; + args.setNOptions(vmOptions.size()); + // The number of elements must still be constant for the stack allocation. + options = StackValue.get(MAX_DEBUGGER_VM_OPTIONS, JNIJavaVMOption.class); + + for (int i = 0; i < vmOptions.size(); i++) { + CCharPointer option = hsVMOptions.get().read(i); + options.addressOf(i).setOptionString(option); + } + + args.setOptions(options); + args.setIgnoreUnrecognized(false); + + args.setVersion(JNIVersion.JNI_VERSION_10()); + args.setIgnoreUnrecognized(true); + + JNIJavaVMPointer jvmptr = StackValue.get(JNIJavaVMPointer.class); + JNIEnvironmentPointer dbgEnv = StackValue.get(JNIEnvironmentPointer.class); + + PlatformNativeLibrarySupport.NativeLibrary library = PlatformNativeLibrarySupport.singleton().createLibrary(libraryPath.toString(), false); + library.load(); + + DebuggingOnDemandHandler.JNICreateJavaVMPointer symCreateJavaVM = (DebuggingOnDemandHandler.JNICreateJavaVMPointer) library.findSymbol("JNI_CreateJavaVM"); + + int result = symCreateJavaVM.call(jvmptr, dbgEnv, args); + + if (result != JNIErrors.JNI_OK()) { + Log.log().string("CreateJavaVM failed: ").signed(result).newline(); + LibC.exit(LibC.EXIT_CODE_ABORT); + } + + debuggerServerJavaVM = (JNI.JavaVM) jvmptr.read(); + + JNIObjectHandle jhJDWPServerClass = dbgEnv.read().getFunctions().getFindClass().invoke(dbgEnv.read(), declaringClass.get()); + abortOnJNIException(dbgEnv); + + JNIMethodId jdwpCreateInstance = dbgEnv.read().getFunctions().getGetStaticMethodID().invoke(dbgEnv.read(), jhJDWPServerClass, hsMethodName.get(), hsSignature.get()); + abortOnJNIException(dbgEnv); + + DebuggingOnDemandHandler.CallStaticObjectMethodFunctionPointer entryPointFunctionPointer = (DebuggingOnDemandHandler.CallStaticObjectMethodFunctionPointer) dbgEnv.read().getFunctions() + .getCallStaticObjectMethod(); + JNI.JObject handle = entryPointFunctionPointer.invoke(dbgEnv.read(), jhJDWPServerClass, jdwpCreateInstance); + abortOnJNIException(dbgEnv); + + JDWPEventHandlerBridge jdwpEventHandler = NativeToHSJDWPEventHandlerBridge.createNativeToHS((JNI.JNIEnv) dbgEnv.read(), handle); + abortOnJNIException(dbgEnv); + + long isolate = CurrentIsolate.getIsolate().rawValue(); + + // Ensures that the callbacks To HotSpot have a JNIEnv available via JNIMethodScope. + Interpreter.DEBUGGER.setEventHandler(new EnterHSEventHandler(jdwpEventHandler)); + + ThreadStartDeathSupport.get().setListener(new ThreadStartDeathSupport.Listener() { + @Override + public void threadStarted() { + IsolateThread isolateThread = CurrentIsolate.getCurrentThread(); + Thread thread = ThreadStartDeathSupport.get().filterAppThread(isolateThread); + if (thread == null) { + return; + } + long threadId = JDWPBridgeImpl.getIds().toId(thread); + ResidentJDWP.LOGGER.log("[StartDeathSupport] threadStarted[" + threadId + "](" + thread.getName() + ")"); + try (JNIMethodScope ignored = new JNIMethodScope("JDWPServer::onThreadStart", currentThreadJniEnv())) { + jdwpEventHandler.onThreadStart(threadId); + } + } + + @Override + @Uninterruptible(reason = "Used in ThreadListenerSupport.", calleeMustBe = false) + public void threadDied() { + IsolateThread isolateThread = CurrentIsolate.getCurrentThread(); + // We need to inform the server through JNI, interruptible code is called + threadDied(isolateThread); + } + + private void threadDied(IsolateThread isolateThread) { + Thread thread = ThreadStartDeathSupport.get().filterAppThread(isolateThread); + if (thread == null) { + return; + } + long threadId = JDWPBridgeImpl.getIds().toId(thread); + ResidentJDWP.LOGGER.log("[StartDeathSupport] threadDied[" + threadId + "](" + thread.getName() + ")"); + try (JNIMethodScope ignored = new JNIMethodScope("JDWPServer::onThreadDeath", currentThreadJniEnv())) { + jdwpEventHandler.onThreadDeath(threadId); + } + } + + @Override + public void vmDied() { + ResidentJDWP.LOGGER.log("[StartDeathSupport] vmDied()"); + try (JNIMethodScope ignored = new JNIMethodScope("JDWPServer::onVMDeath", currentThreadJniEnv())) { + jdwpEventHandler.onVMDeath(); + } + } + + }); + + assert Interpreter.DEBUGGER.getEventHandler() != null; + Path metadataPath = DebuggerSupport.getMetadataFilePath(); + String metadataHashString = DebuggerSupport.getMetadataHashString(); + try (JNIMethodScope ignored = new JNIMethodScope("JDWPServer::spawnServer", currentThreadJniEnv())) { + long jdwpBridgeHandle = createJDWPBridgeHandle(); + jdwpEventHandler.spawnServer( + jdwpOptions.jdwpOptions(), + jdwpOptions.additionalOptions(), + isolate, + initialThreadId, + jdwpBridgeHandle, + metadataHashString, + metadataPath.toString(), + jdwpOptions.tracing()); + } + } + } + + /** + * Finds a Java home from environment variables. + * + * @return path to Java home, or {@code null} if cannot be found + */ + static Path findJavaHome() { + String javaHome = System.getenv("JDWP_SERVER_JAVA_HOME"); + if (javaHome == null) { + javaHome = System.getenv("GRAALVM_HOME"); + if (javaHome == null) { + javaHome = System.getenv("JAVA_HOME"); + } + } + return (javaHome != null) + ? Path.of(javaHome) + : null; + } + + /** + * Finds the given {@link System#mapLibraryName(String) library name} (file name), in the given + * search paths. The search paths are inspected in the given order, search paths can point to + * regular files or directories. + * + *

+ * A file search path is valid, if it exists and have the same file name as the specified + * library name. A directory search is valid, if it exists contains the specified library file + * name. + * + * @param libraryName file name of the library, platform-dependent, as given by + * {@link System#mapLibraryName(String)} + * @param throwIfNotFound flag to throw {@link IllegalArgumentException} if the library is not + * found + * @param searchPaths paths to search, files of directories + * @return the path of the library, guaranteed to be an existing regular file, or null if the + * library was not found (and {@code throwIfNotFound} is false). + * + * @throws IllegalArgumentException if {@code throwIfNotFound} and the library was not found in + * the given search paths + */ + private static Path findLibrary(String libraryName, boolean throwIfNotFound, List searchPaths) { + for (Path path : searchPaths) { + Path fileName = path.getFileName(); + if (Files.isRegularFile(path) && fileName != null && libraryName.equals(fileName.toString())) { + return path; + } else if (Files.isDirectory(path)) { + Path libraryPath = path.resolve(libraryName); + if (Files.isRegularFile(libraryPath)) { + return libraryPath; + } + } + } + + if (throwIfNotFound) { + throw new IllegalArgumentException(libraryName + " not found in search path: " + searchPaths); + } + + return null; + } + + /** + * Returns the platform-dependent path (Linux/Mac/Windows) to the native libraries directory in + * a Java home. + * + * @return path to the native libraries directory, or {@code null} if the Java home is + * {@code null} + */ + private static Path librariesInJavaHome(Path javaHome) { + if (javaHome == null) { + return null; + } + return javaHome.resolve(OS.WINDOWS.isCurrent() ? "bin" : "lib"); + } + + /** + * Returns the platform-dependent path (Linux/Mac/Windows) to {@code lib:jvm} library within a + * Java home. A Java home may contain several implementations of {@code lib:jvm} e.g. + * {@code "server" | "client" | "truffle"}, use {@code jvmSubDirectory} to select which one. + * + * @return path to the lib:jvm shared library, or {@code null} if the Java home is {@code null} + */ + private static Path jvmLibraryInJavaHome(Path javaHome, String jvmSubDirectory) { + Path libraries = librariesInJavaHome(javaHome); + if (libraries == null) { + return null; + } + return libraries.resolve(jvmSubDirectory); + } + + /** + * Returns the platform-dependent path (Linux/Mac/Windows) to {@code lib:jvm} library within a + * Java home. Picks the {@ocde lib:jvm} "server" configuration. + * + * @return path to the lib:jvm shared library, or {@code null} if the Java home is {@code null} + */ + private static Path jvmLibraryInJavaHome(Path javaHome) { + return jvmLibraryInJavaHome(javaHome, "server"); + } + + private static List filterValidPaths(Path... paths) { + List validPaths = new ArrayList<>(); + for (Path path : paths) { + if (path != null && Files.exists(path)) { + validPaths.add(path); + } + } + return validPaths; + } + + /** + * Finds the native library path used launch the JDWP server. Supports both + * {@link Options#mode() native or jvm modes}. + * + *

+ * In {@link Options#mode() native mode}, the search paths are as follows: + *

    + *
  1. The specified {@link Options#libraryPath() library path} + *
  2. The native executable directory, NOT the current working directory
  3. + *
  4. Assumes {@link Options#libraryPath() library path} is a Java home, search for + * {@code lib:svmjdwp} there. + *
  5. Finds a Java home from environment variables, search for {@code lib:svmjdwp} there. + *
+ * + *

+ * In {@link Options#mode() jvm mode}, the search paths are as follows: + *

    + *
  1. The specified {@link Options#libraryPath() library path} + *
  2. Assumes {@link Options#libraryPath() library path} is a Java home, search for + * {@code lib:jvm} there. + *
  3. Finds a Java home from environment variables, search for {@code lib:jvm} there. + *
+ * + * @throws IllegalArgumentException if the native library cannot be found + * + * @return path the to native library used to run the JDWP server, {@code lib:svmjdwp} or + * {@code lib:jvm} depending on the {@link Options#mode()} + */ + private static Path findLibraryPath(Options jdwpOptions) { + Path libraryPath = null; + if (jdwpOptions.libraryPath() != null) { + libraryPath = Path.of(jdwpOptions.libraryPath()); + } + + String libraryName; + List searchPaths; + if ("jvm".equals(jdwpOptions.mode())) { + libraryName = System.mapLibraryName("jvm"); + searchPaths = filterValidPaths( + libraryPath, + jvmLibraryInJavaHome(libraryPath), + jvmLibraryInJavaHome(findJavaHome())); + } else { + assert "native".equals(jdwpOptions.mode()); + libraryName = System.mapLibraryName("svmjdwp"); + searchPaths = filterValidPaths( + libraryPath, + Path.of(ProcessProperties.getExecutableName()).getParent(), + librariesInJavaHome(libraryPath), + librariesInJavaHome(findJavaHome())); + } + + return findLibrary(libraryName, true, searchPaths); + } + + private static long createJDWPBridgeHandle() { + VMError.guarantee(JDWPOptions.JDWP.getValue()); + VMError.guarantee(InterpreterOptions.DebuggerWithInterpreter.getValue()); + return NativeObjectHandles.create(new JDWPBridgeImpl()); + } + + static long toPrimitiveOrId(JavaKind kind, Object object) { + return switch (kind) { + case Boolean -> JavaConstant.forBoolean((boolean) object).getRawValue(); + case Byte -> JavaConstant.forByte((byte) object).getRawValue(); + case Short -> JavaConstant.forShort((short) object).getRawValue(); + case Char -> JavaConstant.forChar((char) object).getRawValue(); + case Int -> JavaConstant.forInt((int) object).getRawValue(); + case Float -> JavaConstant.forFloat((float) object).getRawValue(); + case Long -> JavaConstant.forLong((long) object).getRawValue(); + case Double -> JavaConstant.forDouble((double) object).getRawValue(); + case Object -> JDWPBridgeImpl.getIds().toId(object); + case Void -> { + assert object == null; + yield 0L; // null + } + case Illegal -> throw VMError.shouldNotReachHere("illegal return kind"); + }; + } + + private static void abortOnJNIException(JNIEnvironmentPointer dbgEnv) { + if (dbgEnv.read().getFunctions().getExceptionCheck().invoke(dbgEnv.read())) { + dbgEnv.read().getFunctions().getExceptionDescribe().invoke(dbgEnv.read()); + LibC.exit(LibC.EXIT_CODE_ABORT); + } + } + + public static boolean suspendDoneOnShellSide() { + Thread t = suspendThread; + suspendThread = null; + if (t != null) { + t.interrupt(); + return true; + } else { + return false; + } + } + + /* + * GR-55105: This shouldn't be needed. Only inner classes are processed by + * com.oracle.svm.hosted.ImageClassLoader#findSystemElements for some reason. + */ + @SuppressWarnings("unused") + private static class EntryPointHolder { + @Uninterruptible(reason = "Dummy symbol to make HotSpot's native method linking to look for symbols in the main executable. Required to run the JDWP server (debugger) on HotSpot") + @CEntryPoint(name = "JNI_OnLoad_DEFAULT_NAMESPACE", include = ResidentJDWPFeatureEnabled.class) + @CEntryPointOptions(prologue = CEntryPointOptions.NoPrologue.class, epilogue = CEntryPointOptions.NoEpilogue.class) + @SuppressWarnings("unused") + public static int onLoadDefaultNamespace(JNI.JavaVM vm, PointerBase reserved) { + return JNIVersion.JNI_VERSION_10(); + } + + @CEntryPoint(name = "Java_com_oracle_svm_jdwp_bridge_JDWPJNIConfig_attachCurrentThread", // + builtin = CEntryPoint.Builtin.ATTACH_THREAD, // + include = ResidentJDWPFeatureEnabled.class) + @SuppressWarnings("unused") + public static native IsolateThread attachCurrentThread(JNI.JNIEnv jniEnv, JNI.JClass clazz, Isolate isolate); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/DebuggingOnDemandHook.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/DebuggingOnDemandHook.java new file mode 100644 index 000000000000..913c745d30af --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/DebuggingOnDemandHook.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import com.oracle.svm.core.jdk.RuntimeSupport; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.MetadataUtil; +import com.oracle.svm.jdwp.bridge.DebugOptions; + +public class DebuggingOnDemandHook implements RuntimeSupport.Hook { + + static final String ADDITIONAL_JDWP_OPTIONS_ENV_VARIABLE = "_NATIVE_JDWP_OPTIONS"; + + @Override + public void execute(boolean isFirstIsolate) { + // TODO(peterssen): GR-55057 Support attaching the JDWP debugger to any isolate, not only + // the first. + if (isFirstIsolate) { + + String jdwpOptions = JDWPOptions.JDWPOptions.getValue(); + if (jdwpOptions == null) { + // Debugger not requested. + return; + } + + // Options can be added externally via this environment variable. Anything contained in + // it will get a comma prepended to it (if needed), then it will be added to the end of + // the JDWP options. + // This mimics the _JAVA_JDWP_OPTIONS env. variable used in HotSpot. + // Note that a different variable name is used here to avoid collisions with options + // meant for the HotSpot JDWP agent. + String nativeJDWPOptions = System.getenv(ADDITIONAL_JDWP_OPTIONS_ENV_VARIABLE); + DebugOptions.Options options = null; + try { + options = DebugOptions.parse(jdwpOptions, nativeJDWPOptions, true, JDWPOptions.JDWPTrace.getValue()); + } catch (IllegalArgumentException e) { + String errorMessage = MetadataUtil.fmt("ERROR: JDWP option syntax error: %s %s=%s", + jdwpOptions, + ADDITIONAL_JDWP_OPTIONS_ENV_VARIABLE, + nativeJDWPOptions); + System.err.println(errorMessage); + System.exit(1); + } + + // TODO(peterssen): GR-55061 Add support for starting JDWP debugging on signal. + // Debug on signal (USR2) allowing to attach to a running executable, + // but it's not clear to me how this is supposed to work and what's the tooling + // involved. + // Signal.handle(new Signal("USR2"), new DebuggingOnDemandHandler(options)); + new DebuggingOnDemandHandler(options).spawnJDWPThread(); + } else { + VMError.shouldNotReachHere("debugging only works in main isolate, for now"); + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/EnterHSEventHandler.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/EnterHSEventHandler.java new file mode 100644 index 000000000000..b8b3128b7fcb --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/EnterHSEventHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import com.oracle.svm.interpreter.debug.EventHandler; +import com.oracle.svm.jdwp.bridge.jniutils.JNIMethodScope; +import com.oracle.svm.jdwp.bridge.EventHandlerBridge; +import com.oracle.svm.jdwp.bridge.TagConstants; +import com.oracle.svm.jdwp.bridge.TypeTag; + +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +public class EnterHSEventHandler implements EventHandler { + private final EventHandlerBridge jdwpEventHandler; + + public EnterHSEventHandler(EventHandlerBridge jdwpEventHandler) { + this.jdwpEventHandler = jdwpEventHandler; + } + + @SuppressWarnings("try") + @Override + public void onEventAt(Thread thread, ResolvedJavaMethod method, int bci, Object returnValue, int eventKindFlags) { + try (JNIMethodScope ignored = new JNIMethodScope("JDWPServer::onEventAt", DebuggingOnDemandHandler.currentThreadJniEnv())) { + long threadId = JDWPBridgeImpl.getIds().toId(thread); + long methodId = JDWPBridgeImpl.getIds().toId(method); + ResolvedJavaType declaringType = method.getDeclaringClass(); + long classId = JDWPBridgeImpl.getIds().toId(declaringType); + byte typeTag = TypeTag.getKind(declaringType); + JavaKind returnKind = method.getSignature().getReturnKind(); + long returnPrimitiveOrId; + byte returnTag; + if (returnValue == null) { + returnPrimitiveOrId = 0; + if (JavaKind.Void == returnKind) { + returnTag = TagConstants.VOID; + } else { + returnTag = TagConstants.OBJECT; + } + } else { + returnPrimitiveOrId = DebuggingOnDemandHandler.toPrimitiveOrId(returnKind, returnValue); + Class returnClass = switch (returnKind) { + case Byte -> Byte.TYPE; + case Boolean -> Boolean.TYPE; + case Char -> Character.TYPE; + case Short -> Short.TYPE; + case Int -> Integer.TYPE; + case Float -> Float.TYPE; + case Long -> Long.TYPE; + case Double -> Double.TYPE; + case Void -> Void.TYPE; + default -> returnValue.getClass(); + }; + returnTag = TagConstants.getTagFromClass(returnClass); + } + assert TagConstants.isValidTag(returnTag) : returnTag; + jdwpEventHandler.onEventAt(threadId, classId, typeTag, methodId, bci, returnTag, returnPrimitiveOrId, eventKindFlags); + } + } + +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPBridgeImpl.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPBridgeImpl.java new file mode 100644 index 000000000000..fc836e993406 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPBridgeImpl.java @@ -0,0 +1,408 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import com.oracle.svm.core.code.FrameInfoQueryResult; +import com.oracle.svm.core.code.FrameSourceInfo; +import com.oracle.svm.core.heap.VMOperationInfos; +import com.oracle.svm.core.interpreter.InterpreterFrameSourceInfo; +import com.oracle.svm.core.thread.JavaVMOperation; +import com.oracle.svm.core.thread.PlatformThreads; +import com.oracle.svm.core.thread.ThreadSuspendSupport; +import com.oracle.svm.core.thread.VMOperation; +import com.oracle.svm.core.thread.VMThreads; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.DebuggerSupport; +import com.oracle.svm.interpreter.debug.DebuggerEvents; +import com.oracle.svm.interpreter.debug.EventKind; +import com.oracle.svm.interpreter.debug.Location; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; +import com.oracle.svm.interpreter.metadata.MetadataUtil; +import com.oracle.svm.jdwp.bridge.ErrorCode; +import com.oracle.svm.jdwp.bridge.JDWP; +import com.oracle.svm.jdwp.bridge.JDWPBridge; +import com.oracle.svm.jdwp.bridge.JDWPException; +import com.oracle.svm.jdwp.bridge.Packet; +import com.oracle.svm.jdwp.bridge.StackFrame; +import com.oracle.svm.jdwp.bridge.TypeTag; +import com.oracle.svm.jdwp.resident.impl.AllJavaFramesVisitor; +import com.oracle.svm.jdwp.resident.impl.ResidentJDWP; +import com.oracle.svm.jdwp.resident.impl.SafeStackWalker; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.IsolateThread; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.OptionalInt; + +public final class JDWPBridgeImpl implements JDWPBridge { + + JDWP impl = new ResidentJDWP(); + + private static final ObjectIdMap IDS = new ObjectIdMap(); + + public static ObjectIdMap getIds() { + return IDS; + } + + private static final DebuggerEvents DEBUGGER = MetadataUtil.requireNonNull(ImageSingletons.lookup(DebuggerEvents.class)); + + @Override + public void setEventEnabled(long threadId, int eventKind, boolean enable) { + DEBUGGER.setEventEnabled(getIds().toObject(threadId, Thread.class), EventKind.fromOrdinal(eventKind), enable); + } + + @Override + public boolean isEventEnabled(long threadId, int eventKind) { + return DEBUGGER.isEventEnabled(getIds().toObject(threadId, Thread.class), EventKind.fromOrdinal(eventKind)); + } + + @Override + public void toggleBreakpoint(long methodId, int bci, boolean enable) { + DEBUGGER.toggleBreakpoint(getIds().toObject(methodId, ResolvedJavaMethod.class), bci, enable); + } + + @Override + public void toggleMethodEnterEvent(long clazzId, boolean enable) { + DEBUGGER.toggleMethodEnterEvent(getIds().toObject(clazzId, ResolvedJavaType.class), enable); + } + + @Override + public void toggleMethodExitEvent(long clazzId, boolean enable) { + DEBUGGER.toggleMethodExitEvent(getIds().toObject(clazzId, ResolvedJavaType.class), enable); + } + + @Override + public void setSteppingFromLocation(long threadId, int depth, int size, long methodId, int bci, int lineNumber) { + DEBUGGER.setSteppingFromLocation(getIds().toObject(threadId, Thread.class), depth, size, Location.create(getIds().toObject(methodId, ResolvedJavaMethod.class), bci, lineNumber)); + } + + @Override + public void clearStepping(long threadId) { + DEBUGGER.clearStepping(getIds().toObject(threadId, Thread.class)); + } + + @Override + public Packet dispatch(Packet packet) throws JDWPException { + return impl.dispatch(packet); + } + + @Override + public int getThreadStatus(long threadId) { + Thread thread = ResidentJDWP.getThread(threadId); + return JDWPThreadStatus.getThreadStatus(thread); + } + + @Override + public long threadSuspend(long threadId) { + return threadSuspendOrResume(true, threadId); + } + + @Override + public long threadResume(long threadId) { + return threadSuspendOrResume(false, threadId); + } + + private static long threadSuspendOrResume(boolean suspend, long threadId) { + Thread thread = ResidentJDWP.getThread(threadId); + if (!thread.isAlive()) { + // An invalid thread (zombie) + return -1; + } + if (suspend) { + ThreadSuspendSupport.suspend(thread); + } else { + ThreadSuspendSupport.resume(thread); + } + return 1; + } + + @Override + public void setThreadRequest(boolean start, boolean enable) { + ThreadStartDeathSupport.get().setListeningOn(start, enable); + } + + @Override + public long[] vmSuspend(long[] ignoredThreadIds) { + SuspendAllOperation operation = new SuspendAllOperation(ignoredThreadIds); + operation.enqueue(); + return operation.getSuspendedThreadIds(); + } + + @Override + public void vmResume(long[] resumeThreadIds) { + if (!DebuggingOnDemandHandler.suspendDoneOnShellSide()) { + ResumeAllOperation operation = new ResumeAllOperation(resumeThreadIds); + operation.enqueue(); + } + } + + private static class SuspendAllOperation extends JavaVMOperation { + + private final Thread[] ignoredThreads; + private Thread[] suspendedThreads; + private int suspendedThreadsCount; + + SuspendAllOperation(long[] ignoredThreadIds) { + super(VMOperationInfos.get(SuspendAllOperation.class, "All Threads Suspend", VMOperation.SystemEffect.SAFEPOINT)); + ignoredThreads = new Thread[ignoredThreadIds.length]; + for (int i = 0; i < ignoredThreadIds.length; i++) { + ignoredThreads[i] = JDWPBridgeImpl.getIds().toObject(ignoredThreadIds[i], Thread.class); + } + } + + @Override + protected void operate() { + Thread[] threads = new Thread[10]; + int i = 0; + threadsLoop: for (IsolateThread thread = VMThreads.firstThreadUnsafe(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) { + Thread t = ThreadStartDeathSupport.get().filterAppThread(thread); + if (t == null || PlatformThreads.getThreadStatus(t) == com.oracle.svm.core.thread.ThreadStatus.TERMINATED) { + continue; + } + for (Thread ignoredThread : ignoredThreads) { + if (ignoredThread == t) { + continue threadsLoop; + } + } + ThreadSuspendSupport.suspend(t); + if (i >= threads.length) { + threads = Arrays.copyOf(threads, i + i / 2); + } + threads[i++] = t; + } + suspendedThreads = threads; + suspendedThreadsCount = i; + } + + long[] getSuspendedThreadIds() { + long[] ids = new long[suspendedThreadsCount]; + for (int i = 0; i < ids.length; i++) { + ids[i] = JDWPBridgeImpl.getIds().getIdOrCreateWeak(suspendedThreads[i]); + } + return ids; + } + } + + private static class ResumeAllOperation extends JavaVMOperation { + + private final Thread[] resumeThreads; + + ResumeAllOperation(long[] resumeThreadIds) { + super(VMOperationInfos.get(ResumeAllOperation.class, "All Threads Resume", VMOperation.SystemEffect.SAFEPOINT)); + resumeThreads = new Thread[resumeThreadIds.length]; + for (int i = 0; i < resumeThreadIds.length; i++) { + resumeThreads[i] = JDWPBridgeImpl.getIds().toObject(resumeThreadIds[i], Thread.class); + } + } + + @Override + protected void operate() { + for (Thread thread : resumeThreads) { + if (thread == null) { + continue; + } + ThreadSuspendSupport.resume(thread); + } + } + } + + @Override + public String getSystemProperty(String key) { + return System.getProperty(key); + } + + @Override + public long typeRefIndexToId(int typeRefIndex) { + ResolvedJavaType typeAtIndex = DebuggerSupport.singleton().getUniverse().getTypeAtIndex(typeRefIndex); + return getIds().getIdOrCreateWeak(typeAtIndex); + } + + @Override + public long fieldRefIndexToId(int fieldRefIndex) { + ResolvedJavaField fieldAtIndex = DebuggerSupport.singleton().getUniverse().getFieldAtIndex(fieldRefIndex); + return getIds().getIdOrCreateWeak(fieldAtIndex); + } + + @Override + public long methodRefIndexToId(int methodRefIndex) { + ResolvedJavaMethod methodAtIndex = DebuggerSupport.singleton().getUniverse().getMethodAtIndex(methodRefIndex); + return getIds().getIdOrCreateWeak(methodAtIndex); + } + + @Override + public int typeRefIdToIndex(long typeRefId) { + ResolvedJavaType resolvedJavaType = null; + try { + resolvedJavaType = getIds().toObject(typeRefId, ResolvedJavaType.class); + } catch (ClassCastException e) { + throw JDWPException.raise(ErrorCode.INVALID_CLASS); + } + if (resolvedJavaType == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + OptionalInt methodRefIndex = DebuggerSupport.singleton().getUniverse().getTypeIndexFor(resolvedJavaType); + return methodRefIndex.orElseThrow(() -> JDWPException.raise(ErrorCode.INVALID_CLASS)); + } + + @Override + public int fieldRefIdToIndex(long fieldRefId) { + ResolvedJavaField resolvedJavaField = null; + try { + resolvedJavaField = getIds().toObject(fieldRefId, ResolvedJavaField.class); + } catch (ClassCastException e) { + throw JDWPException.raise(ErrorCode.INVALID_FIELDID); + } + if (resolvedJavaField == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + OptionalInt typeRefIndex = DebuggerSupport.singleton().getUniverse().getFieldIndexFor(resolvedJavaField); + return typeRefIndex.orElseThrow(() -> JDWPException.raise(ErrorCode.INVALID_FIELDID)); + } + + @Override + public int methodRefIdToIndex(long methodRefId) { + ResolvedJavaMethod resolvedJavaMethod = null; + try { + resolvedJavaMethod = getIds().toObject(methodRefId, ResolvedJavaMethod.class); + } catch (ClassCastException e) { + throw JDWPException.raise(ErrorCode.INVALID_METHODID); + } + if (resolvedJavaMethod == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + OptionalInt methodRefIndex = DebuggerSupport.singleton().getUniverse().getMethodIndexFor(resolvedJavaMethod); + return methodRefIndex.orElseThrow(() -> JDWPException.raise(ErrorCode.INVALID_METHODID)); + } + + @Override + public String currentWorkingDirectory() { + return Path.of("").toAbsolutePath().toString(); + } + + @Override + public int[] typeStatus(long... typeIds) { + int[] result = new int[typeIds.length]; + for (int i = 0; i < typeIds.length; i++) { + long typeId = typeIds[i]; + Object type = getIds().getObject(typeId); + if (type instanceof ResolvedJavaType resolvedJavaType) { + // Check that the type is part of the interpreter universe, e.g. arbitrary + // ResolvedJavaType instances are not valid. + OptionalInt typeIndex = DebuggerSupport.singleton().getUniverse().getTypeIndexFor(resolvedJavaType); + if (typeIndex.isEmpty()) { + throw JDWPException.raise(ErrorCode.INVALID_CLASS); + } + result[i] = ClassUtils.getStatus(resolvedJavaType); + } else { + throw JDWPException.raise(ErrorCode.INVALID_CLASS); + } + } + return result; + } + + @Override + public StackFrame[] getThreadFrames(long threadId) { + Thread targetThread = JDWPBridgeImpl.getIds().toObject(threadId, Thread.class); + AllJavaFramesVisitor getAllFrames = new AllJavaFramesVisitor(true); + SafeStackWalker.safeStackWalk(targetThread, getAllFrames); + + List frames = getAllFrames.getFrames(); + + StackFrame[] stackFrames = new StackFrame[frames.size()]; + for (int i = 0; i < stackFrames.length; i++) { + FrameSourceInfo frameInfo = frames.get(i); + + Class sourceClass = frameInfo.getSourceClass(); + ResolvedJavaType sourceType = DebuggerSupport.singleton().getUniverse().lookupType(sourceClass); + ResolvedJavaMethod sourceMethod = findSourceMethod(sourceType, frameInfo); + + byte typeTag = TypeTag.getKind(sourceType); + long classId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(sourceType); + long methodId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(sourceMethod); + int bci = frameInfo.getBci(); + + // Workaround for JDI crash, causes by illegal BCI on stack traces. + // SVM native C API may produce non-native methods that have no bytecodes, but report + // BCI=1 on stack traces. + // Ensure that methods without bytecodes always report unknown BCI in stack traces. + if (!sourceMethod.hasBytecodes()) { + bci = -1; + } + + int frameDepth = i; + stackFrames[i] = new StackFrame(typeTag, classId, methodId, bci, frameDepth); + } + + return stackFrames; + } + + @Override + public long getCurrentThis() { + Object thisObject = ResidentJDWP.getThis(Thread.currentThread(), 0); + return JDWPBridgeImpl.getIds().getIdOrCreateWeak(thisObject); + } + + @Override + public boolean isCurrentThreadVirtual() { + return Thread.currentThread().isVirtual(); + } + + /** + * Returns the {@link InterpreterResolvedJavaMethod} associated with the given stack frame. + * + *

+ * Interpreter frames contains a direct reference to the associated + * {@link InterpreterResolvedJavaMethod}. Compiled frames contains a + * {@link FrameInfoQueryResult#getSourceMethodId()} that can be used to obtain the interpreter + * method via {@link InterpreterUniverse#getMethodFromMethodId(int)}. + */ + public static ResolvedJavaMethod findSourceMethod(ResolvedJavaType sourceType, FrameSourceInfo frameInfo) { + if (frameInfo instanceof InterpreterFrameSourceInfo interpreterFrameSourceInfo) { + // Interpreter frames contain the interpreted method. + assert sourceType.equals(interpreterFrameSourceInfo.getInterpretedMethod().getDeclaringClass()); + return interpreterFrameSourceInfo.getInterpretedMethod(); + } else if (frameInfo instanceof FrameInfoQueryResult compiledFrameInfo) { + // Compiled frames have a methodId. + int sourceMethodId = compiledFrameInfo.getSourceMethodId(); + if (sourceMethodId != 0) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + ResolvedJavaMethod interpreterMethod = universe.getMethodFromMethodId(sourceMethodId); + if (interpreterMethod != null) { + assert sourceType.equals(interpreterMethod.getDeclaringClass()); + assert interpreterMethod.getName().equals(frameInfo.getSourceMethodName()); + return interpreterMethod; + } + } + } + + throw VMError.shouldNotReachHere("Cannot find method " + frameInfo.getSourceMethodName() + " in class " + frameInfo.getSourceClassName() + " at line " + frameInfo.getSourceLineNumber()); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPOptions.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPOptions.java new file mode 100644 index 000000000000..90dad8cb5f08 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPOptions.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import com.oracle.svm.interpreter.InterpreterOptions; + +import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.core.option.RuntimeOptionKey; + +import com.oracle.svm.interpreter.debug.DebuggerEventsFeature; +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionKey; +import jdk.graal.compiler.options.OptionType; +import org.graalvm.collections.EconomicMap; + +public final class JDWPOptions { + + @Option(help = "Include JDWP support in the native executable", type = OptionType.Expert)// + public static final HostedOptionKey JDWP = new HostedOptionKey<>(false) { + + @Override + protected void onValueUpdate(EconomicMap, Object> values, Boolean oldValue, Boolean newValue) { + super.onValueUpdate(values, oldValue, newValue); + if (newValue) { + InterpreterOptions.DebuggerWithInterpreter.update(values, true); + DebuggerEventsFeature.DebuggerOptions.DebuggerEvents.update(values, true); + } + } + }; + + @Option(help = "Specify JDWP options")// + public static final RuntimeOptionKey JDWPOptions = new RuntimeOptionKey<>(null); + + @Option(help = "Enable JDWP specifc logging", type = OptionType.Expert) // + public static final RuntimeOptionKey JDWPTrace = new RuntimeOptionKey<>(false); +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPThreadStatus.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPThreadStatus.java new file mode 100644 index 000000000000..b133d5ac793c --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/JDWPThreadStatus.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import com.oracle.svm.core.thread.PlatformThreads; +import com.oracle.svm.core.thread.ThreadStatus; + +public enum JDWPThreadStatus { + + ZOMBIE(0), + RUNNING(1), + SLEEPING(2), + MONITOR(3), + WAIT(4), + NOT_STARTED(5); + + public final int value; + + JDWPThreadStatus(int value) { + this.value = value; + } + + public static int getThreadStatus(Thread thread) { + int state = PlatformThreads.getThreadStatus(thread); + JDWPThreadStatus status = switch (state) { + case ThreadStatus.NEW -> NOT_STARTED; + case ThreadStatus.BLOCKED_ON_MONITOR_ENTER -> MONITOR; + case ThreadStatus.IN_OBJECT_WAIT, ThreadStatus.IN_OBJECT_WAIT_TIMED, ThreadStatus.PARKED, ThreadStatus.PARKED_TIMED -> WAIT; + case ThreadStatus.SLEEPING -> SLEEPING; + case ThreadStatus.TERMINATED -> ZOMBIE; + default -> RUNNING; + }; + return status.value; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ObjectIdMap.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ObjectIdMap.java new file mode 100644 index 000000000000..557bc8bd4123 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ObjectIdMap.java @@ -0,0 +1,888 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +import jdk.internal.misc.Unsafe; + +import jdk.graal.compiler.core.common.SuppressFBWarnings; +import org.graalvm.nativeimage.ImageSingletons; + +/** + * Mapping of objects to unique IDs and back. Allows concurrent access and automatically disposes + * mapping of GCed values. IDs are linearly increasing long values. + *

+ * This is a lock-free implementation. The {@link LockFreeHashMap} accesses the hash table via an + * 'accessFlag', which assures exclusive resize of the table. The resize is done by prepending a + * bigger hash table to the table chain. After resize, new objects are always put to the first and + * the biggest table. + *

+ * Objects are stored in arrays of {@link HashNode} nodes. The arrays must not be muted to assure + * data consistency. When nodes need to be changed, a new array is created and is safely (CAS) + * replaced in the table index. + *

+ * Every inserted object gets a unique ID assigned. The ID is generated only after the object's node + * is stored in a table to assure uniqueness of IDs. See + * {@link HashNode#finalizeId(ObjectIdMap.LockFreeHashMap, ObjectIdMap.LockFreeHashMap.HashingTable)}. + */ +public final class ObjectIdMap { + + private static final int INITIAL_SIZE_BITS = 10; // The first table has size 2^INITIAL_SIZE_BITS + // An attemt to resize when the current resizeCount % (table.length/RESIZE_ATTEMPT) == 0 + private static final int RESIZE_ATTEMPT = 16; + + private volatile LockFreeHashMap map; + private static final long mapOffset = Unsafe.getUnsafe().objectFieldOffset(ObjectIdMap.class, "map"); + + private static long getObjectArrayByteOffset(long index) { + int offset = Unsafe.getUnsafe().arrayBaseOffset(Object[].class); + int scale = Unsafe.getUnsafe().arrayIndexScale(Object[].class); + try { + return Math.addExact(offset, Math.multiplyExact(index, scale)); + } catch (ArithmeticException ex) { + throw new IndexOutOfBoundsException(index); + } + } + + @SuppressWarnings("unchecked") + private static T getElementVolatile(T[] array, long index) { + long arrayByteOffset = getObjectArrayByteOffset(index); + return (T) Unsafe.getUnsafe().getReferenceVolatile(array, arrayByteOffset); + } + + private static boolean compareAndSetElement(Object[] array, long index, Object existingElement, Object newElement) { + long arrayByteOffset = getObjectArrayByteOffset(index); + return Unsafe.getUnsafe().compareAndSetReference(array, arrayByteOffset, existingElement, newElement); + } + + private LockFreeHashMap getMap() { + LockFreeHashMap theMap = map; + if (theMap == null) { + // We have no map yet + theMap = new LockFreeHashMap(); + LockFreeHashMap oldMap = (LockFreeHashMap) Unsafe.getUnsafe().compareAndExchangeReference(this, mapOffset, null, theMap); + if (oldMap != null) { + // It was set already + theMap = oldMap; + } + } + return theMap; + } + + /** + * Returns the ID, or -1 when the object is not tracked. + */ + public long getIdExisting(Object obj) { + if (obj == null) { + return 0; + } + return getMap().getIdExisting(obj); + } + + public long getIdOrCreateWeak(Object obj) { + return getMap().getIdOrCreateWeak(obj); + } + + public Object getObject(long id) { + if (id == 0) { + return null; + } + return getMap().getObject(id); + } + + /** + * Decreases the hold count by one, replaces HashNodeStrong with HashNodeWeak when holdCount is + * zero. + * + * @param id The object id + * @return true when the object reference exists. + */ + public boolean enableCollection(long id) { + return enableCollection(id, 1, false); + } + + /** + * Decreases the hold count by {@code disposeIfNotHold} and when holdCount is zero then either + * dispose the node, or replace HashNodeStrong with HashNodeWeak. + * + * @param id The object id + * @param refCount the count to decrease the hold count by. + * @param disposeIfNotHold whether to dispose the object ID when hold count decrements to zero. + * @return true when the object reference existed. + */ + public boolean enableCollection(long id, int refCount, boolean disposeIfNotHold) { + return getMap().enableCollection(id, refCount, disposeIfNotHold); + } + + /** + * Increases the hold count by one, replaces HashNodeWeak with HashNodeStrong when holdCount is + * zero. + * + * @param id The object id + * @return true when the object reference exists. + */ + public boolean disableCollection(long id) { + return getMap().disableCollection(id); + } + + /** + * Check if the object was collected. + * + * @param id The object id. + * @return true when the object was collected, false when the object + * still exists in memory, null when the id never referenced an object. + */ + @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "Intentional.") + public Boolean isCollected(long id) { + return getMap().isCollected(id); + } + + public void reset() { + LockFreeHashMap theMap; + do { + theMap = map; + if (theMap == null) { + return; + } + } while (!Unsafe.getUnsafe().compareAndSetReference(this, mapOffset, theMap, null)); + // Reset 'theMap', it will not be used any more + theMap.reset(); + } + + public T toObject(long objectId, Class targetClass) { + Object object = getObject(objectId); + return targetClass.cast(object); + } + + public long toId(Object object) { + return getIdOrCreateWeak(object); + } + + private static class LockFreeHashMap { + + private volatile TableAccessFlag accessFlag; + private static final long accessFlagOffset = Unsafe.getUnsafe().objectFieldOffset(LockFreeHashMap.class, "accessFlag"); + private final ReferenceQueue refQueue = new ReferenceQueue<>(); + private volatile Thread refQueueThread; + private volatile long lastId; + private static final long lastIdOffset = Unsafe.getUnsafe().objectFieldOffset(LockFreeHashMap.class, "lastId"); + + LockFreeHashMap() { + } + + long getNextId() { + long id = lastId; + do { + long witnessId = Unsafe.getUnsafe().compareAndExchangeLong(this, lastIdOffset, id, id + 1); + if (witnessId != id) { + // Try again + id = witnessId; + } else { + // id + 1 was written successfully + return id + 1; + } + } while (true); + } + + private Thread startCleanupThread() { + Thread queueThread = Thread.ofPlatform().name("JDWP Object map cleanup queue").unstarted(() -> { + while (true) { + try { + Reference ref = refQueue.remove(); + HashNode node = (HashNodeWeak) ref; + dispose(node); + } catch (InterruptedException ex) { + break; + } + } + }); + if (ImageSingletons.contains(ThreadStartDeathSupport.class)) { + ThreadStartDeathSupport.get().setDebuggerThreadObjectQueue(queueThread); + } + queueThread.setDaemon(true); + queueThread.start(); + return queueThread; + } + + void reset() { + Thread queueThread = refQueueThread; + if (queueThread != null) { + queueThread.interrupt(); + if (ImageSingletons.contains(ThreadStartDeathSupport.class)) { + ThreadStartDeathSupport.get().setDebuggerThreadObjectQueue(null); + } + } + // GC of this object and the ReferenceQueue will make all its elements eligible for GC. + if (queueThread != null) { + try { // The queue thread should finish eventually. + queueThread.join(); + } catch (InterruptedException e) { + // The join was interrupted, we give up + } + } + } + + private TableAccessFlag getTableAccess() { + TableAccessFlag flag = accessFlag; + if (flag == null) { + // No table was created yet, create the first one: + HashingTable table = new HashingTable(1 << INITIAL_SIZE_BITS, null); + flag = new TableAccessFlag(0, table); + TableAccessFlag oldFlag = (TableAccessFlag) Unsafe.getUnsafe().compareAndExchangeReference(this, accessFlagOffset, null, flag); + if (oldFlag == null) { + // We have successfully set the first table + refQueueThread = startCleanupThread(); + } else { + // It was set already + flag = oldFlag; + } + } + return flag; + } + + private boolean setNewTableAccess(TableAccessFlag oldFlag, TableAccessFlag newFlag) { + return Unsafe.getUnsafe().compareAndSetReference(this, accessFlagOffset, oldFlag, newFlag); + } + + /** + * Returns the ID, or -1 when the object is not tracked. + */ + public long getIdExisting(Object obj) { + if (obj == null) { + return 0; + } + TableAccessFlag flag = accessFlag; + if (flag == null) { + return -1; // Have no tables yet + } + HashingTable table = flag.table(); + int hash = System.identityHashCode(obj); + HashNode node = getIdExisting(table, obj, hash); + if (node != null) { + return node.getId(); + } else { + return -1; + } + } + + private static HashNode getIdExisting(HashingTable table, Object obj, int hash) { + for (HashingTable t = table; t != null; t = t.getNext()) { + HashNode node = t.getIdExisting(obj, hash); + if (node != null) { + return node; + } + } + return null; + } + + public long getIdOrCreateWeak(Object obj) { + long id = getIdExisting(obj); + if (id != -1) { + return id; + } + + int hash = System.identityHashCode(obj); + HashNode node = null; + boolean[] needsFinalizeId = new boolean[]{false}; + do { + TableAccessFlag tableAccess = getTableAccess(); + HashingTable table = tableAccess.table(); + boolean needsResize = table.needsResize(); + if (needsResize) { + int hashTableLength = table.hashToObjectTable.length; + if (tableAccess.resizeCount % (hashTableLength / RESIZE_ATTEMPT) == 0) { + // Let's try to do resize + int newSize = table.hashToObjectTable.length << 1; + HashingTable newTable = new HashingTable(newSize, table); + TableAccessFlag newTableAccess = new TableAccessFlag(0, newTable); + if (!setNewTableAccess(tableAccess, newTableAccess)) { + // The resize was not successful + continue; + } else { + // We're resized + table = newTable; + tableAccess = newTableAccess; + } + } else { + // Just increase the resize request count + TableAccessFlag newTableAccess = new TableAccessFlag(accessFlag.resizeCount + 1, table); + if (!setNewTableAccess(tableAccess, newTableAccess)) { + // Try next time + continue; + } + } + } + // Write the value to the table + node = table.getIdOrCreateWeak(obj, hash, needsFinalizeId); + if (needsFinalizeId[0]) { + // A new node was written to the table. + // We need to verify that the table wasn't resized in between + if (tableAccess.table == accessFlag.table) { + // We wrote it into the current table, great. + // Assign a new ID: + node.finalizeId(this, table); + } else { + // The table was resized in between. We can not be sure + // whether it wasn't put into the new table already + continue; + } + } + // We have the object's node. We exit the loop if the ID is set. + assert node != null; + } while (node == null || node.getId() < 0); + + return node.getId(); + } + + public Object getObject(long id) { + if (id == 0) { + return null; + } + TableAccessFlag flag = accessFlag; + if (flag == null) { + return null; // Have no tables yet + } + for (HashingTable table = flag.table(); table != null; table = table.getNext()) { + Object obj = table.getObject(id); + if (obj != null) { + return obj; + } + } + return null; + } + + private void dispose(HashNode node) { + TableAccessFlag flag = accessFlag; + if (flag == null) { + return; // Have no tables yet + } + disposeAll(flag.table(), node); + } + + private static void disposeAll(HashingTable table, HashNode node) { + for (HashingTable t = table; t != null; t = t.getNext()) { + t.dispose(node); + } + } + + boolean enableCollection(long id, int refCount, boolean disposeIfNotHold) { + if (refCount < 0) { + throw new IllegalArgumentException("Negative refCount not permitted: " + refCount); + } + TableAccessFlag flag = accessFlag; + if (flag == null) { + return false; // Have no tables yet + } + for (HashingTable table = flag.table(); table != null; table = table.getNext()) { + boolean sucess = table.enableCollection(id, refCount, disposeIfNotHold); + if (sucess) { + return sucess; + } + } + return false; + } + + public boolean disableCollection(long id) { + TableAccessFlag flag = accessFlag; + if (flag == null) { + return false; // Have no tables yet + } + for (HashingTable table = flag.table(); table != null; table = table.getNext()) { + boolean sucess = table.disableCollection(id); + if (sucess) { + return sucess; + } + } + return false; + } + + @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "Intentional.") + public Boolean isCollected(long id) { + if (id <= 0 || id > lastId) { + // Non-existing object + return null; + } + Object obj = getObject(id); + return null == obj; + } + + /** + * This class contains the hashing table that stores the hash -> object mapping and also + * the ID -> hash code mapping for lookups by the ID. + *

+ * hashToObjectTable is an array indexed by object's system hash code and + * contains arrays of HashNode.
+ * idToHashTable is an array indexed by unique IDs and contains lists of + * object's hash codes. + */ + final class HashingTable { + + private final HashNode[][] hashToObjectTable; // object table by a hash code index + private final HashListNode[] idToHashTable; // hash values by ID index + private final HashingTable next; + private volatile int size; + private static final long sizeOffset = Unsafe.getUnsafe().objectFieldOffset(HashingTable.class, "size"); + + HashingTable(int size, HashingTable next) { + idToHashTable = new HashListNode[size]; + hashToObjectTable = new HashNode[size][]; + this.next = next; + } + + HashingTable getNext() { + return next; + } + + HashNode getIdExisting(Object obj, int hash) { + int index = (hashToObjectTable.length - 1) & hash; + HashNode[] chain = getElementVolatile(hashToObjectTable, index); + if (chain != null) { + for (HashNode node : chain) { + if (obj == node.getObject()) { + return node; + } + } + } + return null; + } + + private void incrementSize() { + changeSize(+1); + } + + private void decrementSize() { + changeSize(-1); + } + + private void changeSize(int increment) { + int oldSize = size; + int s; + do { + s = Unsafe.getUnsafe().compareAndExchangeInt(this, sizeOffset, oldSize, oldSize + increment); + if (s != oldSize) { + // Try again + oldSize = s; + } else { + break; + } + } while (true); + } + + boolean needsResize() { + return size > hashToObjectTable.length && (hashToObjectTable.length << 1) > 0; + } + + private HashNode getIdOrCreateWeak(Object obj, int hash, boolean[] needsFinalizeId) { + int index = (hashToObjectTable.length - 1) & hash; + HashNode[] oldChain; + HashNode[] newChain; + do { + oldChain = getElementVolatile(hashToObjectTable, index); + if (oldChain != null) { + // Search the old node for the object + for (HashNode node : oldChain) { + if (node.getObject() == obj) { + // The node is there already + needsFinalizeId[0] = false; + return node; + } + } + newChain = new HashNode[oldChain.length + 1]; + System.arraycopy(oldChain, 0, newChain, 1, oldChain.length); + } else { + newChain = new HashNode[1]; + } + newChain[0] = new HashNodeWeak(hash, obj, refQueue); + } while (!compareAndSetElement(hashToObjectTable, index, oldChain, newChain)); + incrementSize(); + // A new node with uninitialized ID + needsFinalizeId[0] = true; + return newChain[0]; + } + + void newId(HashNode node) { + // A node got a new ID assigned. + // We need to add that to our 'hashes' table. + long newId = node.getId(); + int hash = node.getHash(); + int hashIndex = (int) ((idToHashTable.length - 1) & newId); + HashListNode oldList; + HashListNode newList; + do { + oldList = getElementVolatile(idToHashTable, hashIndex); + newList = new HashListNode(hash, oldList); + } while (!compareAndSetElement(idToHashTable, hashIndex, oldList, newList)); + } + + public Object getObject(long id) { + if (id == 0) { + return null; + } + int hashIndex = (int) ((idToHashTable.length - 1) & id); + HashListNode list = getElementVolatile(idToHashTable, hashIndex); + while (list != null) { + int hash = list.hash(); + int index = (hashToObjectTable.length - 1) & hash; + HashNode[] chain = getElementVolatile(hashToObjectTable, index); + if (chain != null) { + for (HashNode node : chain) { + if (id == node.getId()) { + return node.getObject(); + } + } + } + list = list.next(); + } + return null; + } + + private void dispose(HashNode node) { + long id = node.getId(); + int hash = node.getHash(); + int index = (hashToObjectTable.length - 1) & hash; + retryLoop: do { + HashNode[] oldChain = getElementVolatile(hashToObjectTable, index); + if (oldChain != null) { + for (int i = 0; i < oldChain.length; i++) { + if (oldChain[i].getId() == id) { + // Remove 'i' element from the oldChain, forming a newChain + HashNode[] newChain; + if (oldChain.length == 1) { + newChain = null; + } else { + // Copy chain, skipping the removed node at position 'i' + newChain = removeNode(oldChain, i); + } + if (!compareAndSetElement(hashToObjectTable, index, oldChain, newChain)) { + // We failed to write the new chain, try again + continue retryLoop; + } else { + // Successfully removed. + // Node with the given ID is in the table just once. + disposeId(id, hash); + decrementSize(); + break; + } + } + } + } + // not found + break; + } while (true); + } + + private static HashNode[] removeNode(HashNode[] oldChain, int index) { + HashNode[] newChain = new HashNode[oldChain.length - 1]; + if (index > 0) { + System.arraycopy(oldChain, 0, newChain, 0, index); + } + if (index < oldChain.length) { + System.arraycopy(oldChain, index + 1, newChain, index, newChain.length - index); + } + return newChain; + } + + private static HashNode[] replaceNode(HashNode[] oldChain, int index, HashNode newNode) { + if (oldChain.length == 1) { + return new HashNode[]{newNode}; + } + HashNode[] newChain = new HashNode[oldChain.length]; + // Copy it all for simplicity + System.arraycopy(oldChain, 0, newChain, 0, oldChain.length); + newChain[index] = newNode; + return newChain; + } + + private void disposeId(long id, int hash) { + int hashIndex = (int) ((idToHashTable.length - 1) & id); + HashListNode oldList; + retryLoop: do { + oldList = getElementVolatile(idToHashTable, hashIndex); + HashListNode nPrev = null; + for (HashListNode n = oldList; n != null; nPrev = n, n = n.next()) { + if (n.hash() == hash) { + // Remove 'n' from the chain + HashListNode newList; + if (nPrev == null) { + newList = n.next(); + } else { + HashListNode end = n.next(); + while (nPrev != null) { + HashListNode nn = new HashListNode(nPrev.hash(), end); + end = nn; + HashListNode lastPrev = nPrev; + // The chains are short, find the previous + nPrev = null; + for (HashListNode on = oldList; on != lastPrev; on = on.next()) { + nPrev = on; + } + } + newList = end; + } + if (compareAndSetElement(idToHashTable, hashIndex, oldList, newList)) { + // Disposed. + return; + } else { + // We failed to write the new chain, try again + continue retryLoop; + } + } + } + // not found + break; + } while (true); + } + + boolean enableCollection(long id, int refCount, boolean disposeIfNotHold) { + int hashIndex = (int) ((idToHashTable.length - 1) & id); + HashListNode list = getElementVolatile(idToHashTable, hashIndex); + for (; list != null; list = list.next()) { + int index = (hashToObjectTable.length - 1) & list.hash(); + retryLoop: do { + HashNode[] chain = getElementVolatile(hashToObjectTable, index); + if (chain != null) { + for (int i = 0; i < chain.length; i++) { + HashNode node = chain[i]; + if (id == node.getId()) { + if (node instanceof HashNodeStrong strongNode) { + int holdCount = strongNode.changeHoldCount(-refCount); + if (holdCount == 0 || holdCount == Integer.MIN_VALUE) { + // The node needs to be replaced with a weak one. + if (disposeIfNotHold) { + disposeAll(this, node); + } else { + // Replace the strong node with a weak one + HashNode weak = new HashNodeWeak(node.getHash(), node.getObject(), refQueue, node.getId()); + HashNode[] newChain = replaceNode(chain, i, weak); + if (compareAndSetElement(hashToObjectTable, index, chain, newChain)) { + // We changed the node. We must wipe out any + // occurrences of this ID from other tables + if (this.next != null) { + disposeAll(this.next, node); + } + } else { + continue retryLoop; + } + } + } + } else if (disposeIfNotHold) { + // We're weak already + disposeAll(this, node); + } + return true; // We found the ID + } + } + } + break; // not found + } while (true); + } + return false; + } + + public boolean disableCollection(long id) { + int hashIndex = (int) ((idToHashTable.length - 1) & id); + HashListNode list = getElementVolatile(idToHashTable, hashIndex); + for (; list != null; list = list.next()) { + int index = (hashToObjectTable.length - 1) & list.hash(); + retryLoop: do { + HashNode[] chain = getElementVolatile(hashToObjectTable, index); + if (chain != null) { + for (int i = 0; i < chain.length; i++) { + HashNode node = chain[i]; + if (id == node.getId()) { + if (node instanceof HashNodeStrong strongNode) { + strongNode.changeHoldCount(+1); + } else { + Object obj = node.getObject(); + if (obj == null) { + // GCed + return false; + } + // Replace node with HashNodeStrong + HashNode strong = new HashNodeStrong(node.getHash(), obj, node.getId()); + HashNode[] newChain = replaceNode(chain, i, strong); + if (compareAndSetElement(hashToObjectTable, index, chain, newChain)) { + // We changed the node. We must wipe out any occurrences + // of this ID from other tables + if (this.next != null) { + disposeAll(this.next, node); + } + } else { + continue retryLoop; + } + } + return true; // We found the ID + } + } + } + break; // not found + } while (true); + } + return false; + } + + } + + private record TableAccessFlag(int resizeCount, HashingTable table) { + + } + + } + + private sealed interface HashNode permits HashNodeWeak, HashNodeStrong { + + /** + * Get a unique ID, or -1 if it was not assigned yet. + */ + long getId(); + + /** + * Assign a new unique ID. + */ + long finalizeId(LockFreeHashMap map, LockFreeHashMap.HashingTable table); + + /** + * Get the object's hash code. + */ + int getHash(); + + /** + * Get the object, or {@code null} when collected. + */ + Object getObject(); + + } + + private static final class HashNodeWeak extends WeakReference implements HashNode { + + private volatile long id; + private final int hash; + + HashNodeWeak(int hash, Object referent, ReferenceQueue refQueue) { + this(hash, referent, refQueue, -1); + } + + HashNodeWeak(int hash, Object referent, ReferenceQueue refQueue, long id) { + super(referent, refQueue); + this.id = id; + this.hash = hash; + } + + @Override + public long getId() { + return id; + } + + @Override + public long finalizeId(LockFreeHashMap map, LockFreeHashMap.HashingTable table) { + long theId = id; + if (theId <= -1) { + theId = map.getNextId(); + // Only the node's creator will finalize, pure set is safe + id = theId; + } + table.newId(this); + return theId; + } + + @Override + public int getHash() { + return hash; + } + + @Override + public Object getObject() { + return get(); + } + + } + + private static final class HashNodeStrong implements HashNode { + + private final long id; + private final int hash; + private final Object object; + private volatile int holdCount = 1; + private static final long holdCountOffset = Unsafe.getUnsafe().objectFieldOffset(HashNodeStrong.class, "holdCount"); + + HashNodeStrong(int hash, Object object, long id) { + this.id = id; + this.hash = hash; + this.object = object; + } + + @Override + public long getId() { + return id; + } + + @Override + public long finalizeId(LockFreeHashMap map, LockFreeHashMap.HashingTable table) { + // The strong nodes are used as a replacement of existing weak nodes, + // which have the ID initialized already. + throw new UnsupportedOperationException(); + } + + int changeHoldCount(int increment) { + int oldCount = holdCount; + if (oldCount == 0) { + // Locked when reached 0, the node is replaced with a weak one. + return Integer.MIN_VALUE; + } + int count; + do { + int newCount = oldCount + increment; + if (newCount < 0) { + newCount = 0; + } + count = Unsafe.getUnsafe().compareAndExchangeInt(this, holdCountOffset, oldCount, newCount); + if (count != oldCount) { + // Try again + oldCount = count; + } else { + return newCount; + } + } while (true); + } + + @Override + public int getHash() { + return hash; + } + + @Override + public Object getObject() { + return object; + } + + } + + private record HashListNode(int hash, HashListNode next) { + } + +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ResidentJDWPFeature.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ResidentJDWPFeature.java new file mode 100644 index 000000000000..4be25fab7807 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ResidentJDWPFeature.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import java.util.List; + +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.jdwp.bridge.jniutils.NativeBridgeSupport; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.hosted.Feature; + +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.jdk.RuntimeSupport; +import com.oracle.svm.interpreter.InterpreterFeature; +import com.oracle.svm.interpreter.debug.DebuggerEventsFeature; +import com.oracle.svm.jdwp.bridge.JDWPNativeBridgeSupport; +import com.oracle.svm.jdwp.bridge.ResidentJDWPFeatureEnabled; + +@Platforms(Platform.HOSTED_ONLY.class) +@AutomaticallyRegisteredFeature +final class ResidentJDWPFeature implements InternalFeature { + + @Override + public String getDescription() { + return "Support debugging native images via JDWP, using standard Java tooling"; + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + registerStartupHook(); + ImageSingletons.add(NativeBridgeSupport.class, new JDWPNativeBridgeSupport()); + ImageSingletons.add(ResidentJDWPFeatureEnabled.class, new ResidentJDWPFeatureEnabled()); + } + + private static void registerStartupHook() { + RuntimeSupport.getRuntimeSupport().addStartupHook(new DebuggingOnDemandHook()); + } + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return JDWPOptions.JDWP.getValue(); + } + + @Override + public List> getRequiredFeatures() { + return List.of(InterpreterFeature.class, DebuggerEventsFeature.class); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ThreadStartDeathSupport.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ThreadStartDeathSupport.java new file mode 100644 index 000000000000..16e226e1b3e5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/ThreadStartDeathSupport.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident; + +import com.oracle.svm.core.Uninterruptible; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.IsolateThread; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + +import com.oracle.svm.core.feature.AutomaticallyRegisteredImageSingleton; +import com.oracle.svm.core.jdk.RuntimeSupport; +import com.oracle.svm.core.thread.PlatformThreads; +import com.oracle.svm.core.thread.ThreadListener; +import com.oracle.svm.core.thread.ThreadListenerSupport; +import com.oracle.svm.core.thread.VMOperationControl; + +import jdk.graal.compiler.api.replacements.Fold; + +/** + * Support for Thread start/death events. + */ +@AutomaticallyRegisteredImageSingleton +public final class ThreadStartDeathSupport implements ThreadListener { + + /** + * Debugger-related threads that should not be visible to clients. + */ + private final Thread[] debuggerThreads = new Thread[2]; + private static final int DEBUGGER_THREAD_INDEX_SERVER = 0; + private static final int DEBUGGER_THREAD_INDEX_OBJECTS_QUEUE = 1; + + private volatile boolean start; + private volatile boolean death; + private volatile Listener listener; + + @Platforms(Platform.HOSTED_ONLY.class) + ThreadStartDeathSupport() { + ThreadListenerSupport.get().register(this); + RuntimeSupport.getRuntimeSupport().addShutdownHook(new RuntimeSupport.Hook() { + @Override + public void execute(boolean isFirstIsolate) { + Listener l = listener; + if (l != null) { + l.vmDied(); + } + } + }); + } + + @Fold + public static ThreadStartDeathSupport get() { + return ImageSingletons.lookup(ThreadStartDeathSupport.class); + } + + void setDebuggerThreadServer(Thread serverThread) { + debuggerThreads[DEBUGGER_THREAD_INDEX_SERVER] = serverThread; + } + + void setDebuggerThreadObjectQueue(Thread serverThread) { + debuggerThreads[DEBUGGER_THREAD_INDEX_OBJECTS_QUEUE] = serverThread; + } + + /** + * Filter application-related threads and convert IsolateThread to Thread. + * + * @return a converted application Thread, or {@code null}. + */ + public Thread filterAppThread(IsolateThread isolateThread) { + if (VMOperationControl.isDedicatedVMOperationThread(isolateThread)) { + return null; + } + Thread thread = PlatformThreads.fromVMThread(isolateThread); + if (thread == null) { + return null; + } + for (Thread t : debuggerThreads) { + if (t == thread) { + return null; + } + } + // TODO(peterssen): GR-55071 Identify external threads that entered via JNI. + if (thread.getName().startsWith("System-")) { + return null; + } + return thread; + } + + void setListener(Listener listener) { + this.listener = listener; + } + + void setListeningOn(boolean startOrDeath, boolean enable) { + if (startOrDeath) { + start = enable; + } else { + death = enable; + } + } + + @Override + public void beforeThreadRun() { + Listener l = listener; + if (l != null && start) { + l.threadStarted(); + } + } + + @Override + @Uninterruptible(reason = "Only uninterruptible because we need to prevent stack overflow errors.") + public void afterThreadRun() { + Listener l = listener; + if (l != null && death) { + l.threadDied(); + } + } + + interface Listener { + + void threadStarted(); + + void threadDied(); + + void vmDied(); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/api/StackframeDescriptor.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/api/StackframeDescriptor.java new file mode 100644 index 000000000000..37285d1dbcd3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/api/StackframeDescriptor.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident.api; + +import com.oracle.svm.core.code.FrameSourceInfo; +import org.graalvm.word.Pointer; + +public interface StackframeDescriptor { + FrameSourceInfo getFrameSourceInfo(); + + Pointer getStackPointer(); +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/AbstractJDWPJavaFrameInfoVisitor.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/AbstractJDWPJavaFrameInfoVisitor.java new file mode 100644 index 000000000000..c32f7880867e --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/AbstractJDWPJavaFrameInfoVisitor.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident.impl; + +import com.oracle.svm.core.code.FrameSourceInfo; +import com.oracle.svm.core.jdk.StackTraceUtils; +import com.oracle.svm.core.stack.JavaStackFrameVisitor; +import org.graalvm.word.Pointer; + +abstract class AbstractJDWPJavaFrameInfoVisitor extends JavaStackFrameVisitor { + private final boolean filterExceptions; + int currentFrameDepth; + + AbstractJDWPJavaFrameInfoVisitor(boolean filterExceptions) { + this.filterExceptions = filterExceptions; + this.currentFrameDepth = 0; + } + + protected boolean ignoreFrame(FrameSourceInfo frameInfo) { + if (!StackTraceUtils.shouldShowFrame(frameInfo, false, true, false)) { + /* Always ignore the frame. It is an internal frame of the VM. */ + return true; + + } else if (filterExceptions && currentFrameDepth == 0 && Throwable.class.isAssignableFrom(frameInfo.getSourceClass())) { + /* + * We are still in the constructor invocation chain at the beginning of the stack trace, + * which is also filtered by the Java HotSpot VM. + */ + return true; + } + + String sourceClassName = null; + Class sourceClass = frameInfo.getSourceClass(); + if (sourceClass != null) { + sourceClassName = sourceClass.getName(); + } + if (currentFrameDepth == 0 && (sourceClassName != null && sourceClassName.startsWith("com.oracle.svm.jdwp."))) { + /* + * Ignore frames used by the debugger to spawn events, but only if they would be + * reported as top-most frames. + */ + return true; + } + + return false; + } + + @Override + public boolean visitFrame(FrameSourceInfo frameInfo, Pointer sp) { + if (ignoreFrame(frameInfo)) { + return true; + } + return processFrame(frameInfo, sp); + } + + protected boolean processFrame(@SuppressWarnings("unused") FrameSourceInfo frameInfo, @SuppressWarnings("unused") Pointer sp) { + ++currentFrameDepth; + return true; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/AllJavaFramesVisitor.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/AllJavaFramesVisitor.java new file mode 100644 index 000000000000..60ea9002406f --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/AllJavaFramesVisitor.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident.impl; + +import com.oracle.svm.core.code.FrameSourceInfo; +import org.graalvm.word.Pointer; + +import java.util.ArrayList; +import java.util.List; + +public final class AllJavaFramesVisitor extends AbstractJDWPJavaFrameInfoVisitor { + + private final List frames; + + public AllJavaFramesVisitor(boolean filterExceptions) { + super(filterExceptions); + this.frames = new ArrayList<>(); + } + + @Override + protected boolean processFrame(FrameSourceInfo frameInfo, Pointer sp) { + super.processFrame(frameInfo, sp); + frames.add(frameInfo); + return true; + } + + public List getFrames() { + return frames; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/GetStackframeDescriptorAtDepthVisitor.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/GetStackframeDescriptorAtDepthVisitor.java new file mode 100644 index 000000000000..2e7e94ee8f62 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/GetStackframeDescriptorAtDepthVisitor.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident.impl; + +import com.oracle.svm.core.code.FrameSourceInfo; +import com.oracle.svm.jdwp.resident.api.StackframeDescriptor; +import org.graalvm.word.Pointer; + +public final class GetStackframeDescriptorAtDepthVisitor extends AbstractJDWPJavaFrameInfoVisitor implements StackframeDescriptor { + private final int frameDepth; + private FrameSourceInfo frameSourceInfo; + private Pointer stackPointer; + + @Override + public FrameSourceInfo getFrameSourceInfo() { + return frameSourceInfo; + } + + @Override + public Pointer getStackPointer() { + return stackPointer; + } + + GetStackframeDescriptorAtDepthVisitor(boolean filterExceptions, int frameDepth) { + super(filterExceptions); + this.frameDepth = frameDepth; + } + + @Override + protected boolean processFrame(FrameSourceInfo frameInfo, Pointer sp) { + super.processFrame(frameInfo, sp); + // currentFrameDepth was incremented in super.processFrame(frameInfo, sp). + + if (currentFrameDepth - 1 == frameDepth) { + this.frameSourceInfo = frameInfo; + this.stackPointer = sp; + return false; + } + + return true; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/ResidentJDWP.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/ResidentJDWP.java new file mode 100644 index 000000000000..a6318ff66cb0 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/ResidentJDWP.java @@ -0,0 +1,2149 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident.impl; + +import com.oracle.svm.core.StaticFieldsSupport; +import com.oracle.svm.core.code.FrameInfoQueryResult; +import com.oracle.svm.core.code.FrameSourceInfo; +import com.oracle.svm.core.deopt.DeoptState; +import com.oracle.svm.core.hub.ClassForNameSupport; +import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.interpreter.InterpreterFrameSourceInfo; +import com.oracle.svm.interpreter.DebuggerSupport; +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; +import com.oracle.svm.core.locks.VMMutex; +import com.oracle.svm.core.log.Log; +import com.oracle.svm.core.meta.SubstrateObjectConstant; +import com.oracle.svm.core.thread.VMThreads; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.EspressoFrame; +import com.oracle.svm.interpreter.InterpreterFrame; +import com.oracle.svm.interpreter.InterpreterToVM; +import com.oracle.svm.interpreter.SemanticJavaException; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaField; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaMethod; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedJavaType; +import com.oracle.svm.interpreter.metadata.InterpreterResolvedObjectType; +import com.oracle.svm.interpreter.metadata.MetadataUtil; +import com.oracle.svm.jdwp.bridge.ErrorCode; +import com.oracle.svm.jdwp.bridge.FrameId; +import com.oracle.svm.jdwp.bridge.InvokeOptions; +import com.oracle.svm.jdwp.bridge.JDWP; +import com.oracle.svm.jdwp.bridge.JDWPException; +import com.oracle.svm.jdwp.bridge.Logger; +import com.oracle.svm.jdwp.bridge.Packet; +import com.oracle.svm.jdwp.bridge.SymbolicRefs; +import com.oracle.svm.jdwp.bridge.TagConstants; +import com.oracle.svm.jdwp.bridge.TypeTag; +import com.oracle.svm.jdwp.bridge.WritablePacket; +import com.oracle.svm.jdwp.resident.JDWPBridgeImpl; +import com.oracle.svm.jdwp.resident.ThreadStartDeathSupport; +import com.oracle.svm.jdwp.resident.ClassUtils; +import com.oracle.svm.jdwp.resident.api.StackframeDescriptor; +import jdk.vm.ci.meta.JavaConstant; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.Local; +import jdk.vm.ci.meta.LocalVariableTable; +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; +import org.graalvm.nativeimage.IsolateThread; +import org.graalvm.word.Pointer; +import org.graalvm.word.WordBase; +import org.graalvm.word.WordFactory; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; + +public final class ResidentJDWP implements JDWP { + + private static final boolean LOGGING = false; + public static Logger LOGGER = new Logger(LOGGING, "[ResidentJDWP]", System.err); + + private final SymbolicRefs symbolicRefs = new ResidentSymbolicRefs(); + + public ResidentJDWP() { + } + + /** + * Reads a reference, can be null. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object was collected or the + * object id is invalid + */ + private static Object readReferenceOrNull(Packet.Reader reader) throws JDWPException { + long objectId = reader.readLong(); + Object value = JDWPBridgeImpl.getIds().getObject(objectId); + if (objectId != 0 && value == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + return value; + } + + /** + * Reads an {@link Class#isArray() array}. + * + * @throws JDWPException {@link ErrorCode#INVALID_ARRAY} if the reference is null, + * {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is invalid + */ + private static Object readArray(Packet.Reader reader) throws JDWPException { + Object array = readReferenceOrNull(reader); + if (array == null || !array.getClass().isArray()) { + throw JDWPException.raise(ErrorCode.INVALID_ARRAY); + } + return array; + } + + /** + * Reads an object of the given class. If the object is {@code null} or not and instance of the + * given class, this method throws {@link JDWPException} with the provided error code is thrown. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + private static T readTypedObject(Packet.Reader reader, Class clazz, ErrorCode errorCode) throws JDWPException { + Object object = readReferenceOrNull(reader); + if (clazz.isInstance(object)) { + return clazz.cast(object); + } + throw JDWPException.raise(errorCode); + } + + /** + * Reads a {@link String} reference. If the object is {@code null} or not an instance of + * {@link String}, this method throws {@link JDWPException} with + * {@link ErrorCode#INVALID_STRING}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + private static String readStringObject(Packet.Reader reader) throws JDWPException { + return readTypedObject(reader, String.class, ErrorCode.INVALID_STRING); + } + + /** + * Reads an {@link InterpreterResolvedJavaType}. If the object is {@code null} or not an + * instance of {@link InterpreterResolvedJavaType}, or it is NOT part of the + * {@link DebuggerSupport#getUniverse() interpreter universe}, this method throws + * {@link JDWPException} with {@link ErrorCode#INVALID_CLASS}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + private static InterpreterResolvedJavaType readType(Packet.Reader reader) throws JDWPException { + Object object = readReferenceOrNull(reader); + if (object instanceof InterpreterResolvedJavaType interpreterResolvedJavaType) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt knownInUniverse = universe.getTypeIndexFor(interpreterResolvedJavaType); + if (knownInUniverse.isPresent()) { + return interpreterResolvedJavaType; + } + } + throw JDWPException.raise(ErrorCode.INVALID_CLASS); // cannot be null + } + + /** + * Reads an {@link InterpreterResolvedJavaField}. If the object is {@code null} or not an + * instance of {@link InterpreterResolvedJavaField}, or it is NOT part of the + * {@link DebuggerSupport#getUniverse() interpreter universe}, this method throws + * {@link JDWPException} with {@link ErrorCode#INVALID_FIELDID}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + private static InterpreterResolvedJavaField readField(Packet.Reader reader) throws JDWPException { + Object object = readReferenceOrNull(reader); + if (object instanceof InterpreterResolvedJavaField interpreterResolvedJavaField) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt knownInUniverse = universe.getFieldIndexFor(interpreterResolvedJavaField); + if (knownInUniverse.isPresent()) { + return interpreterResolvedJavaField; + } + } + throw JDWPException.raise(ErrorCode.INVALID_FIELDID); // cannot be null + } + + /** + * Reads an {@link InterpreterResolvedJavaMethod}. If the object is {@code null} or not an + * instance of {@link InterpreterResolvedJavaMethod}, or it is NOT part of the + * {@link DebuggerSupport#getUniverse() interpreter universe}, this method throws + * {@link JDWPException} with {@link ErrorCode#INVALID_METHODID}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + @SuppressWarnings("unused") + private static InterpreterResolvedJavaMethod readMethod(Packet.Reader reader) throws JDWPException { + Object object = readReferenceOrNull(reader); + if (object instanceof InterpreterResolvedJavaMethod interpreterResolvedJavaMethod) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt knownInUniverse = universe.getMethodIndexFor(interpreterResolvedJavaMethod); + if (knownInUniverse.isPresent()) { + return interpreterResolvedJavaMethod; + } + } + throw JDWPException.raise(ErrorCode.INVALID_METHODID); // cannot be null + } + + /** + * Reads a {@link ThreadGroup}. If the object is {@code null} or not an instance of + * {@link ThreadGroup} this method throws {@link JDWPException} with + * {@link ErrorCode#INVALID_THREAD_GROUP}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + private static ThreadGroup readThreadGroup(Packet.Reader reader) throws JDWPException { + return readTypedObject(reader, ThreadGroup.class, ErrorCode.INVALID_THREAD_GROUP); + } + + /** + * Reads a {@link Module}. If the object is {@code null} or not an instance of {@link Module} + * this method throws {@link JDWPException} with {@link ErrorCode#INVALID_MODULE}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + private static Module readModule(Packet.Reader reader) throws JDWPException { + return readTypedObject(reader, Module.class, ErrorCode.INVALID_MODULE); + } + + /** + * Reads a {@link Class}. If the object is {@code null} or not an instance of {@link Class} this + * method throws {@link JDWPException} with {@link ErrorCode#INVALID_CLASS}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + private static Class readClassObject(Packet.Reader reader) throws JDWPException { + return readTypedObject(reader, Class.class, ErrorCode.INVALID_CLASS); + } + + /** + * Reads a {@link ClassLoader}. If the object is {@code null} or not an instance of + * {@link Class} this method throws {@link JDWPException} with + * {@link ErrorCode#INVALID_CLASS_LOADER}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + @SuppressWarnings("unused") + private static ClassLoader readClassLoader(Packet.Reader reader) throws JDWPException { + return readTypedObject(reader, ClassLoader.class, ErrorCode.INVALID_CLASS_LOADER); + } + + /** + * Verify that the given object id is valid and not collected. An object id can be + * {@link SymbolicRefs#NULL null}, which is valid in this method, thus the {@code ...orNull} + * suffix. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + private static Object verifyObjectIdOrNull(long objectId) throws JDWPException { + Object object = JDWPBridgeImpl.getIds().getObject(objectId); + if (object != null || objectId == SymbolicRefs.NULL) { + return object; + } + // Reference was collected, or is unknown. + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + + /** + * Verify that the given object id is a valid {@link InterpreterResolvedJavaType type}, not + * null, not collected and part of the {@link DebuggerSupport#getUniverse() interpreter + * universe}; otherwise it throws {@link JDWPException} with {@link ErrorCode#INVALID_CLASS}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + @SuppressWarnings("unused") + private static ResolvedJavaType verifyRefType(long refTypeId) throws JDWPException { + Object object = verifyObjectIdOrNull(refTypeId); + if (object instanceof InterpreterResolvedJavaType interpreterResolvedJavaType) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt knownInUniverse = universe.getTypeIndexFor(interpreterResolvedJavaType); + if (knownInUniverse.isPresent()) { + return interpreterResolvedJavaType; + } + } + throw JDWPException.raise(ErrorCode.INVALID_CLASS); // cannot be null + } + + /** + * Verify that the given object id is a valid {@link InterpreterResolvedJavaField field}, not + * null, not collected and part of the {@link DebuggerSupport#getUniverse() interpreter + * universe}; otherwise it throws {@link JDWPException} with {@link ErrorCode#INVALID_FIELDID}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + @SuppressWarnings("unused") + private static ResolvedJavaField verifyRefField(long fieldId) throws JDWPException { + Object object = verifyObjectIdOrNull(fieldId); + if (object instanceof InterpreterResolvedJavaField interpreterResolvedJavaField) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt knownInUniverse = universe.getFieldIndexFor(interpreterResolvedJavaField); + if (knownInUniverse.isPresent()) { + return interpreterResolvedJavaField; + } + } + throw JDWPException.raise(ErrorCode.INVALID_FIELDID); // cannot be null + } + + /** + * Verify that the given object id is a valid {@link InterpreterResolvedJavaMethod method}, not + * null, not collected and part of the {@link DebuggerSupport#getUniverse() interpreter + * universe}; otherwise it throws {@link JDWPException} with {@link ErrorCode#INVALID_METHODID}. + * + * @throws JDWPException {@link ErrorCode#INVALID_OBJECT} if the object id was collected or is + * invalid + */ + @SuppressWarnings("unused") + private static ResolvedJavaMethod verifyRefMethod(long methodId) throws JDWPException { + Object object = verifyObjectIdOrNull(methodId); + if (object instanceof InterpreterResolvedJavaMethod interpreterResolvedJavaMethod) { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt knownInUniverse = universe.getMethodIndexFor(interpreterResolvedJavaMethod); + if (knownInUniverse.isPresent()) { + return interpreterResolvedJavaMethod; + } + } + throw JDWPException.raise(ErrorCode.INVALID_METHODID); // cannot be null + } + + /** + * Writes a tagged-object, as specified in the JDWP spec. The {@link TagConstants tag} is + * derived from the given object. {@code null} is tagged as {@link TagConstants#OBJECT}. + */ + private static void writeTaggedObject(Packet.Writer writer, Object value) { + writer.writeByte(TagConstants.getTagFromReference(value)); + writer.writeLong(JDWPBridgeImpl.getIds().getIdOrCreateWeak(value)); + } + + @Override + public Packet VirtualMachine_AllThreads(Packet packet) throws JDWPException { + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + long[] allThreads = getAllThreadIds(); + data.writeInt(allThreads.length); + for (long threadId : allThreads) { + data.writeLong(threadId); + } + + return reply; + } + + private static VMMutex lockThreads() { + VMMutex mutex; + try { + Field mutexField = VMThreads.class.getDeclaredField("THREAD_MUTEX"); + mutexField.setAccessible(true); + mutex = (VMMutex) mutexField.get(null); + } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException ex) { + ex.printStackTrace(); + throw JDWPException.raise(ErrorCode.INTERNAL); + } + mutex.lock(); + return mutex; + } + + private static long[] getAllThreadIds() { + long[] ids = new long[10]; + int i = 0; + VMMutex mutex = lockThreads(); + try { + for (IsolateThread thread = VMThreads.firstThreadUnsafe(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) { + Thread t = ThreadStartDeathSupport.get().filterAppThread(thread); + if (t == null) { + continue; + } + if (i >= ids.length) { + ids = Arrays.copyOf(ids, i + i / 2); + } + ids[i++] = JDWPBridgeImpl.getIds().getIdOrCreateWeak(t); + } + } finally { + mutex.unlock(); + } + ids = Arrays.copyOf(ids, i); + Log.log().string("getAllThreadIds(): " + Arrays.toString(ids)).newline(); + return ids; + } + + @Override + public Packet VirtualMachine_AllClassesWithGeneric(Packet packet) throws JDWPException { + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + ResolvedJavaType[] allClasses = allReferenceTypes(); + + data.writeInt(allClasses.length); + for (ResolvedJavaType type : allClasses) { + data.writeByte(TypeTag.getKind(type)); + data.writeLong(JDWPBridgeImpl.getIds().getIdOrCreateWeak(type)); + data.writeString(ClassUtils.getTypeAsString(type)); + data.writeString(ClassUtils.getGenericTypeAsString(type)); + data.writeInt(ClassUtils.getStatus(type)); + } + + return reply; + } + + @Override + public Packet VirtualMachine_Dispose(Packet packet) throws JDWPException { + JDWPBridgeImpl.getIds().reset(); + return WritablePacket.newReplyTo(packet); + } + + @Override + public Packet VirtualMachine_DisposeObjects(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + int numRequests = input.readInt(); + + for (int i = 0; i < numRequests; i++) { + long objectId = input.readLong(); + int refCount = input.readInt(); + JDWPBridgeImpl.getIds().enableCollection(objectId, refCount, true); + } + + return WritablePacket.newReplyTo(packet); + } + + @Override + public Packet ObjectReference_DisableCollection(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + long objectId = input.readLong(); + + boolean success = JDWPBridgeImpl.getIds().disableCollection(objectId); + if (!success) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + return WritablePacket.newReplyTo(packet); + } + + @Override + public Packet ObjectReference_EnableCollection(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + long objectId = input.readLong(); + + boolean success = JDWPBridgeImpl.getIds().enableCollection(objectId); + if (!success) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + return WritablePacket.newReplyTo(packet); + } + + @Override + public Packet ObjectReference_IsCollected(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + long objectId = input.readLong(); + + Boolean collected = JDWPBridgeImpl.getIds().isCollected(objectId); + if (collected == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + data.writeBoolean(collected); + + return reply; + } + + @Override + public Packet ThreadReference_Name(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + + Thread thread = readThread(input); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + // The thread name. + data.writeString(thread.getName()); + + return reply; + } + + @Override + public Packet ThreadReference_ThreadGroup(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + + Thread thread = readThread(input); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + // The thread group. + ThreadGroup threadGroup = thread.getThreadGroup(); + if (threadGroup == null) { + // Thread has terminated + throw JDWPException.raise(ErrorCode.INVALID_THREAD); + } + long id = JDWPBridgeImpl.getIds().getIdOrCreateWeak(threadGroup); + data.writeLong(id); + + return reply; + } + + private static Thread readThread(Packet.Reader reader) throws JDWPException { + return readTypedObject(reader, Thread.class, ErrorCode.INVALID_THREAD); + } + + public static Thread getThread(long threadId) { + Thread thread; + try { + thread = JDWPBridgeImpl.getIds().toObject(threadId, Thread.class); + } catch (ClassCastException e) { + throw JDWPException.raise(ErrorCode.INVALID_THREAD); + } + if (thread == null) { + if (threadId == 0) { + // A null thread is invalid + throw JDWPException.raise(ErrorCode.INVALID_THREAD); + } else { + // Unknown ID + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + } + return thread; + } + + @Override + public Packet ThreadGroupReference_Name(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + + ThreadGroup threadGroup = readThreadGroup(input); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + // The thread group name. + data.writeString(threadGroup.getName()); + + return reply; + } + + @Override + public Packet ThreadGroupReference_Children(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + + ThreadGroup threadGroup = readThreadGroup(input); + long[] threadIds = new long[10]; + long[] threadGroupIds = new long[10]; + int ti = 0; + int tgi = 0; + VMMutex mutex = lockThreads(); + try { + // Find child threads and child groups: + for (IsolateThread thread = VMThreads.firstThreadUnsafe(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) { + Thread t = ThreadStartDeathSupport.get().filterAppThread(thread); + if (t == null) { + continue; + } + ThreadGroup tg = t.getThreadGroup(); + if (tg == threadGroup) { + // A direct child thread + if (ti >= threadIds.length) { + threadIds = Arrays.copyOf(threadIds, ti + ti / 2); + } + threadIds[ti++] = JDWPBridgeImpl.getIds().getIdOrCreateWeak(t); + } + if (tg != null && tg.getParent() == threadGroup) { + // A direct child thread group + long id = JDWPBridgeImpl.getIds().getIdOrCreateWeak(tg); + boolean contains = false; + for (int i = 0; i < tgi; i++) { + if (threadGroupIds[i] == id) { + contains = true; + break; + } + } + if (!contains) { + if (tgi >= threadGroupIds.length) { + threadGroupIds = Arrays.copyOf(threadGroupIds, tgi + tgi / 2); + } + threadGroupIds[tgi++] = id; + } + } + } + } finally { + mutex.unlock(); + } + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + data.writeInt(ti); + for (int i = 0; i < ti; i++) { + data.writeLong(threadIds[i]); + } + data.writeInt(tgi); + for (int i = 0; i < tgi; i++) { + data.writeLong(threadGroupIds[i]); + } + + return reply; + + } + + @Override + public Packet ThreadGroupReference_Parent(Packet packet) throws JDWPException { + Packet.Reader input = packet.newDataReader(); + ThreadGroup threadGroup = readThreadGroup(input); + assert input.isEndOfInput(); + + ThreadGroup parentGroup = threadGroup.getParent(); + long parentId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(parentGroup); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + data.writeLong(parentId); + + return reply; + } + + @Override + public Packet VirtualMachine_TopLevelThreadGroups(Packet packet) throws JDWPException { + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + long[] threadGroupIds = new long[5]; + int tgi = 0; + VMMutex mutex = lockThreads(); + try { + // Find all top thread groups: + for (IsolateThread thread = VMThreads.firstThreadUnsafe(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) { + Thread t = ThreadStartDeathSupport.get().filterAppThread(thread); + if (t == null) { + continue; + } + ThreadGroup tg = t.getThreadGroup(); + if (tg != null) { + ThreadGroup rootGroup = tg; + while ((tg = tg.getParent()) != null) { + rootGroup = tg; + } + long id = JDWPBridgeImpl.getIds().getIdOrCreateWeak(rootGroup); + boolean contains = false; + for (int i = 0; i < tgi; i++) { + if (threadGroupIds[i] == id) { + contains = true; + break; + } + } + if (!contains) { + // A new top-level group + if (tgi >= threadGroupIds.length) { + threadGroupIds = Arrays.copyOf(threadGroupIds, tgi + tgi / 2); + } + threadGroupIds[tgi++] = id; + } + } + } + } finally { + mutex.unlock(); + } + + data.writeInt(tgi); + for (int i = 0; i < tgi; i++) { + data.writeLong(threadGroupIds[i]); + } + + return reply; + } + + @Override + public Packet VirtualMachine_AllClasses(Packet packet) throws JDWPException { + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + ResolvedJavaType[] allClasses = allReferenceTypes(); + + data.writeInt(allClasses.length); + for (ResolvedJavaType type : allClasses) { + data.writeByte(TypeTag.getKind(type)); + data.writeLong(JDWPBridgeImpl.getIds().getIdOrCreateWeak(type)); + data.writeString(ClassUtils.getTypeAsString(type)); + data.writeInt(ClassUtils.getStatus(type)); + } + + return reply; + } + + private static ResolvedJavaType[] allReferenceTypes() { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + return universe.getTypes() + .stream() + // AllClasses cannot return primitive types. + .filter(type -> !type.isPrimitive()) + .toArray(ResolvedJavaType[]::new); + } + + @Override + public Packet ObjectReference_ReferenceType(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Object object = readReferenceOrNull(reader); + assert reader.isEndOfInput(); + if (object == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + Class c = object.getClass(); + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + ResolvedJavaType type = universe.lookupType(c); + long typeId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(type); + byte typeKind = TypeTag.getKind(type); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer data = reply.dataWriter(); + + data.writeByte(typeKind); + data.writeLong(typeId); + + return reply; + } + + @Override + public Packet VirtualMachine_CreateString(Packet packet) throws JDWPException { + assert packet.commandSet() == JDWP.VirtualMachine; + assert packet.command() == JDWP.VirtualMachine_CreateString; + + Packet.Reader reader = packet.newDataReader(); + String str = reader.readString(); + assert reader.isEndOfInput(); + long stringId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(str); + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeLong(stringId); + return reply; + } + + @Override + public Packet StringReference_Value(Packet packet) throws JDWPException { + assert packet.commandSet() == JDWP.StringReference; + assert packet.command() == JDWP.StringReference_Value; + + Packet.Reader reader = packet.newDataReader(); + String string = readStringObject(reader); + assert reader.isEndOfInput(); + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeString(string); + return reply; + } + + @Override + public Packet ReferenceType_ClassObject(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + assert reader.isEndOfInput(); + + Class javaClass = type.getJavaClass(); + long classId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(javaClass); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeLong(classId); + + return reply; + } + + @Override + public Packet ClassObjectReference_ReflectedType(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Class classObject = readClassObject(reader); + assert reader.isEndOfInput(); + + ResolvedJavaType type = DebuggerSupport.lookupType(classObject); + assert type != null; + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + long typeId = symbolicRefs.toTypeRef(type); + writer.writeByte(TypeTag.getKind(type)); + writer.writeLong(typeId); + return reply; + } + + @Override + public Packet ReferenceType_ClassLoader(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + assert reader.isEndOfInput(); + + Class javaClass = type.getJavaClass(); + ClassLoader classLoader = javaClass.getClassLoader(); + long classLoaderId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(classLoader); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeLong(classLoaderId); + + return reply; + } + + @Override + public Packet ReferenceType_Module(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + assert reader.isEndOfInput(); + + Class javaClass = type.getJavaClass(); + Module module = javaClass.getModule(); + long moduleId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(module); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeLong(moduleId); + + return reply; + } + + @Override + public Packet ModuleReference_Name(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Module module = readModule(reader); + assert reader.isEndOfInput(); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + + String moduleName = module.getName(); + // From HotSpot: The JDWP converts null into an empty string. + if (moduleName == null) { + moduleName = ""; + } + writer.writeString(moduleName); + return reply; + } + + /** + * This method return all the modules in the native image. Note that + * {@code ModuleLayer.boot().modules()} does not include unnamed modules. + */ + private static Module[] allModules() { + Set allModules = new HashSet<>(ModuleLayer.boot().modules()); + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + for (ResolvedJavaType type : universe.getTypes()) { + if (type instanceof InterpreterResolvedObjectType objectType) { + Class javaClass = objectType.getJavaClass(); + if (javaClass != null) { + Module module = javaClass.getModule(); + if (module != null) { + allModules.add(module); + } + } + } + } + return allModules.toArray(Module[]::new); + } + + @Override + public Packet VirtualMachine_AllModules(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + assert reader.isEndOfInput(); + + Module[] allModules = allModules(); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + + writer.writeInt(allModules.length); + for (Module module : allModules) { + long moduleId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(module); + writer.writeLong(moduleId); + } + + return reply; + } + + @Override + public Packet ModuleReference_ClassLoader(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Module module = readModule(reader); + assert reader.isEndOfInput(); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + ClassLoader classLoader = module.getClassLoader(); + long classLoaderId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(classLoader); + writer.writeLong(classLoaderId); + return reply; + } + + @Override + public Packet ReferenceType_NestedTypes(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + assert reader.isEndOfInput(); + + Class javaClass = type.getJavaClass(); + Class[] declaredClasses = javaClass.getDeclaredClasses(); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeInt(declaredClasses.length); + for (Class declaredClass : declaredClasses) { + ResolvedJavaType declaredType = DebuggerSupport.lookupType(declaredClass); + long declaredTypeId = JDWPBridgeImpl.getIds().getIdOrCreateWeak(declaredType); + writer.writeLong(declaredTypeId); + } + + return reply; + } + + @Override + public Packet ThreadReference_IsVirtual(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Thread thread = readThread(reader); + assert reader.isEndOfInput(); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeBoolean(thread.isVirtual()); + return reply; + } + + @Override + public Packet ArrayReference_Length(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Object array = readArray(reader); + assert array.getClass().isArray(); + assert reader.isEndOfInput(); + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + int arrayLength = InterpreterToVM.arrayLength(array); + writer.writeInt(arrayLength); + return reply; + } + + private static void validateArrayRegion(int firstIndex, int length, int arrayLength) { + if (firstIndex < 0 || firstIndex >= arrayLength) { + throw JDWPException.raise(ErrorCode.INVALID_INDEX); + } + if (length < 0 || firstIndex > arrayLength - length) { + throw JDWPException.raise(ErrorCode.INVALID_LENGTH); + } + } + + @Override + public Packet ArrayReference_GetValues(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Object array = readArray(reader); + + int firstIndex = reader.readInt(); + int length = reader.readInt(); + + assert reader.isEndOfInput(); + + int arrayLength = InterpreterToVM.arrayLength(array); + if (length == -1) { + // Read all remaining values. + // Not in the JDWP spec, but included in HotSpot's JDWP implementation. + length = arrayLength - firstIndex; + } + validateArrayRegion(firstIndex, length, arrayLength); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + + Class componentType = array.getClass().getComponentType(); + boolean isWordTypeComponent = WordBase.class.isAssignableFrom(componentType); + + byte componentStorageTag; + if (isWordTypeComponent) { + componentStorageTag = switch (InterpreterToVM.wordJavaKind()) { + case Int -> TagConstants.INT; + case Long -> TagConstants.LONG; + default -> + throw VMError.shouldNotReachHere("Unexpected word kind " + InterpreterToVM.wordJavaKind()); + }; + } else { + componentStorageTag = TagConstants.getTagFromClass(componentType); + } + + // The first byte is a signature byte which is used to identify the type. + writer.writeByte(componentStorageTag); + + // Next is a four-byte integer indicating the number of values in the sequence. + writer.writeInt(length); + + assert firstIndex >= 0; + assert firstIndex < arrayLength; + assert firstIndex <= arrayLength - length; + + // This is followed by the values themselves. + if (isWordTypeComponent) { + for (int i = firstIndex; i - firstIndex < length; ++i) { + WordBase value = InterpreterToVM.getArrayWord(i, (WordBase[]) array); + switch (InterpreterToVM.wordJavaKind()) { + case Int -> writer.writeInt((int) value.rawValue()); + case Long -> writer.writeLong(value.rawValue()); + default -> + throw VMError.shouldNotReachHere("Unexpected word kind " + InterpreterToVM.wordJavaKind()); + } + } + } else if (componentType.isPrimitive()) { + // Primitive values are encoded as a sequence of untagged-values. + for (int i = firstIndex; i - firstIndex < length; ++i) { + switch (componentStorageTag) { + case TagConstants.INT -> { + int value = InterpreterToVM.getArrayInt(i, (int[]) array); + writer.writeInt(value); + } + case TagConstants.FLOAT -> { + float value = InterpreterToVM.getArrayFloat(i, (float[]) array); + writer.writeFloat(value); + } + case TagConstants.DOUBLE -> { + double value = InterpreterToVM.getArrayDouble(i, (double[]) array); + writer.writeDouble(value); + } + case TagConstants.LONG -> { + long value = InterpreterToVM.getArrayLong(i, (long[]) array); + writer.writeLong(value); + } + case TagConstants.BYTE -> { + byte value = InterpreterToVM.getArrayByte(i, array); + writer.writeByte(value); + } + case TagConstants.SHORT -> { + short value = InterpreterToVM.getArrayShort(i, (short[]) array); + writer.writeShort(value); + } + case TagConstants.CHAR -> { + char value = InterpreterToVM.getArrayChar(i, (char[]) array); + writer.writeChar(value); + } + case TagConstants.BOOLEAN -> { + byte value = InterpreterToVM.getArrayByte(i, array); + writer.writeBoolean(value != 0); + } + default -> throw VMError.shouldNotReachHere("Illegal primitive component tag: " + componentStorageTag); + } + } + } else { + // Object values are encoded as a sequence of values. + for (int i = firstIndex; i - firstIndex < length; ++i) { + Object value = InterpreterToVM.getArrayObject(i, (Object[]) array); + writeTaggedObject(writer, value); + } + } + + return reply; + } + + @Override + public Packet ArrayReference_SetValues(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Object array = readArray(reader); + + int firstIndex = reader.readInt(); + int length = reader.readInt(); + int arrayLength = InterpreterToVM.arrayLength(array); + validateArrayRegion(firstIndex, length, arrayLength); + + Class componentType = array.getClass().getComponentType(); + byte componentStorageTag; + boolean isWordTypeComponent = WordBase.class.isAssignableFrom(componentType); + if (isWordTypeComponent) { + componentStorageTag = switch (InterpreterToVM.wordJavaKind()) { + case Int -> TagConstants.INT; + case Long -> TagConstants.LONG; + default -> + throw VMError.shouldNotReachHere("Unexpected word kind " + InterpreterToVM.wordJavaKind()); + }; + } else { + componentStorageTag = TagConstants.getTagFromClass(componentType); + } + + assert firstIndex >= 0; + assert firstIndex < arrayLength; + assert firstIndex <= arrayLength - length; + + // This is followed by the values themselves. + if (isWordTypeComponent) { + for (int i = firstIndex; i - firstIndex < length; ++i) { + switch (InterpreterToVM.wordJavaKind()) { + case Int -> { + WordBase value = WordFactory.signed(reader.readInt()); + InterpreterToVM.setArrayWord(value, i, (WordBase[]) array); + } + case Long -> { + WordBase value = WordFactory.signed(reader.readLong()); + InterpreterToVM.setArrayWord(value, i, (WordBase[]) array); + } + default -> + throw VMError.shouldNotReachHere("Unexpected word kind " + InterpreterToVM.wordJavaKind()); + } + } + } else if (componentType.isPrimitive()) { + // For primitive values, each value's type must match the array component type exactly. + // Primitive values are encoded as a sequence of untagged-values. + for (int i = firstIndex; i - firstIndex < length; ++i) { + switch (componentStorageTag) { + case TagConstants.INT -> { + int value = reader.readInt(); + InterpreterToVM.setArrayInt(value, i, (int[]) array); + } + case TagConstants.FLOAT -> { + float value = reader.readFloat(); + InterpreterToVM.setArrayFloat(value, i, (float[]) array); + } + case TagConstants.DOUBLE -> { + double value = reader.readDouble(); + InterpreterToVM.setArrayDouble(value, i, (double[]) array); + } + case TagConstants.LONG -> { + long value = reader.readLong(); + InterpreterToVM.setArrayLong(value, i, (long[]) array); + } + case TagConstants.BYTE -> { + byte value = (byte) reader.readByte(); + InterpreterToVM.setArrayByte(value, i, array); + } + case TagConstants.SHORT -> { + short value = reader.readShort(); + InterpreterToVM.setArrayShort(value, i, (short[]) array); + } + case TagConstants.CHAR -> { + char value = reader.readChar(); + InterpreterToVM.setArrayChar(value, i, (char[]) array); + } + case TagConstants.BOOLEAN -> { + boolean value = reader.readBoolean(); + InterpreterToVM.setArrayByte(value ? (byte) 1 : (byte) 0, i, array); + } + default -> throw VMError.shouldNotReachHere("Illegal primitive component tag: " + componentStorageTag); + } + } + } else { + // For object values, there must be a widening reference conversion from the value's + // type to the array component type and the array component type must be loaded. + // Object values are encoded as a sequence of untagged-values. + for (int i = firstIndex; i - firstIndex < length; ++i) { + Object value = readReferenceOrNull(reader); + if (value != null && !componentType.isInstance(value)) { + throw JDWPException.raise(ErrorCode.TYPE_MISMATCH); + } + InterpreterToVM.setArrayObject(value, i, (Object[]) array); + } + } + + assert reader.isEndOfInput(); + + return WritablePacket.newReplyTo(packet); // empty reply + } + + @Override + public Packet ArrayType_NewInstance(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + ResolvedJavaType arrayType = readType(reader); + if (!arrayType.isArray()) { + throw JDWPException.raise(ErrorCode.TYPE_MISMATCH); + } + + int length = reader.readInt(); + if (length < 0) { + throw JDWPException.raise(ErrorCode.INVALID_LENGTH); + } + assert reader.isEndOfInput(); + + ResolvedJavaType componentType = arrayType.getComponentType(); + Object array; + try { + if (componentType.isPrimitive()) { + assert componentType.getJavaKind() != JavaKind.Void; + array = InterpreterToVM.createNewPrimitiveArray((byte) componentType.getJavaKind().getBasicType(), length); + } else { + array = InterpreterToVM.createNewReferenceArray((InterpreterResolvedJavaType) componentType, length); + } + } catch (OutOfMemoryError e) { + throw JDWPException.raise(ErrorCode.OUT_OF_MEMORY); + } + + assert array != null; + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writeTaggedObject(writer, array); + return reply; + } + + private static StackframeDescriptor getStackframeDescriptor(Thread thread, int frameDepth) { + assert !thread.isVirtual(); + GetStackframeDescriptorAtDepthVisitor visitor = new GetStackframeDescriptorAtDepthVisitor(false, frameDepth); + SafeStackWalker.safeStackWalk(thread, visitor); + return visitor; + } + + public static Object getThis(Thread thread, int frameDepth) { + StackframeDescriptor stackframeDescriptor = getStackframeDescriptor(thread, frameDepth); + FrameSourceInfo frameSourceInfo = stackframeDescriptor.getFrameSourceInfo(); + require(frameSourceInfo != null, ErrorCode.INVALID_FRAMEID, + "Frame depth %s not found for thread.threadId()=%s", frameDepth, thread.threadId()); + + /* + * Return null for static or native methods; otherwise, the JVM spec guarantees that "this" + * is in slot 0. + */ + Object thisObject; + if (frameSourceInfo instanceof InterpreterFrameSourceInfo interpreterJavaFrameInfo) { + ResolvedJavaMethod method = interpreterJavaFrameInfo.getInterpretedMethod(); + if (method.isStatic() || method.isNative()) { + thisObject = null; + } else { + InterpreterFrame interpreterFrame = (InterpreterFrame) interpreterJavaFrameInfo.getInterpreterFrame(); + thisObject = EspressoFrame.getThis(interpreterFrame); + } + } else { + FrameInfoQueryResult frameInfoQueryResult = (FrameInfoQueryResult) frameSourceInfo; + + if (frameInfoQueryResult.isNativeMethod()) { + thisObject = null; + } else { + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + ResolvedJavaType type = universe.lookupType(frameInfoQueryResult.getSourceClass()); + ResolvedJavaMethod method = JDWPBridgeImpl.findSourceMethod(type, frameInfoQueryResult); + + assert !method.isNative(); + + if (method.isStatic()) { + thisObject = null; + } else { + DeoptState deoptState = new DeoptState(stackframeDescriptor.getStackPointer(), WordFactory.zero()); + JavaConstant javaConstant = deoptState.readLocalVariable(0, frameInfoQueryResult); + thisObject = SubstrateObjectConstant.asObject(javaConstant); + } + } + } + return thisObject; + } + + @Override + public Packet StackFrame_ThisObject(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Thread thread = readThread(reader); + + // The server ensures that the thread is suspended/parked. + // A thread may be parked or even in a RUNNABLE state, but still be considered suspended by + // the debugger. + assert !thread.isVirtual(); + long frameId = reader.readLong(); + // frameId is validated on the server. + int frameDepth = FrameId.getFrameDepth(frameId); + assert reader.isEndOfInput(); + + Object thisObject = getThis(thread, frameDepth); + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writeTaggedObject(writer, thisObject); + return reply; + } + + @Override + public Packet StackFrame_GetValues(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Thread thread = readThread(reader); + assert !thread.isVirtual(); + + long frameId = reader.readLong(); + int frameDepth = FrameId.getFrameDepth(frameId); + assert frameDepth >= 0; + + int slots = reader.readInt(); + assert slots >= 0; + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + + StackframeDescriptor stackframeDescriptor = getStackframeDescriptor(thread, frameDepth); + FrameSourceInfo frameSourceInfo = stackframeDescriptor.getFrameSourceInfo(); + require(frameSourceInfo != null, ErrorCode.INVALID_FRAMEID, + "Frame depth %s not found for thread.threadId()=%s", frameDepth, thread.threadId()); + + // The number of values retrieved, always equal to slots, the number of values to get. + writer.writeInt(slots); + + Pointer stackPointer = stackframeDescriptor.getStackPointer(); + for (int i = 0; i < slots; i++) { + // The local variable's index in the frame. + int slot = reader.readInt(); + byte tag = JDWP.readTag(reader); + + if (frameSourceInfo instanceof InterpreterFrameSourceInfo interpreterJavaFrameInfo) { + readLocalFromInterpreterFrame(tag, interpreterJavaFrameInfo, slot, writer); + } else { + FrameInfoQueryResult frameInfoQueryResult = (FrameInfoQueryResult) frameSourceInfo; + readLocalFromCompiledFrame(tag, frameInfoQueryResult, stackPointer, slot, writer); + } + } + + assert reader.isEndOfInput(); + + return reply; + } + + private static void sharedReadField(Packet.Writer writer, Object typeOrReceiver, InterpreterResolvedJavaField field) { + + Object receiver; + JavaKind fieldKind = field.getJavaKind(); + if (field.isStatic()) { + assert typeOrReceiver instanceof InterpreterResolvedJavaType; + // typeOrReceiver is ignored, all static fields are grouped together. + receiver = (fieldKind.isPrimitive() || field.getType().isWordType()) + ? StaticFieldsSupport.getStaticPrimitiveFields() + : StaticFieldsSupport.getStaticObjectFields(); + } else { + receiver = typeOrReceiver; + assert receiver != null; + } + + if (field.isUndefined()) { + writer.writeByte(TagConstants.VOID); + return; + } + + assert !field.isUndefined() : "Cannot read undefined field " + field; + + if (field.getType().isWordType()) { + switch (InterpreterToVM.wordJavaKind()) { + case Int -> { + writer.writeByte(TagConstants.INT); + writer.writeInt((int) InterpreterToVM.getFieldWord(receiver, field).rawValue()); + } + case Long -> { + writer.writeByte(TagConstants.LONG); + writer.writeLong(InterpreterToVM.getFieldWord(receiver, field).rawValue()); + } + } + return; + } + + switch (fieldKind) { + case Boolean -> { + writer.writeByte(TagConstants.BOOLEAN); + writer.writeBoolean(InterpreterToVM.getFieldBoolean(receiver, field)); + } + case Byte -> { + writer.writeByte(TagConstants.BYTE); + writer.writeByte(InterpreterToVM.getFieldByte(receiver, field)); + } + case Short -> { + writer.writeByte(TagConstants.SHORT); + writer.writeShort(InterpreterToVM.getFieldShort(receiver, field)); + } + case Char -> { + writer.writeByte(TagConstants.CHAR); + writer.writeChar(InterpreterToVM.getFieldChar(receiver, field)); + } + case Int -> { + writer.writeByte(TagConstants.INT); + writer.writeInt(InterpreterToVM.getFieldInt(receiver, field)); + } + case Float -> { + writer.writeByte(TagConstants.FLOAT); + writer.writeFloat(InterpreterToVM.getFieldFloat(receiver, field)); + } + case Long -> { + writer.writeByte(TagConstants.LONG); + writer.writeLong(InterpreterToVM.getFieldLong(receiver, field)); + } + case Double -> { + writer.writeByte(TagConstants.DOUBLE); + writer.writeDouble(InterpreterToVM.getFieldDouble(receiver, field)); + } + case Object -> { + Object value = InterpreterToVM.getFieldObject(receiver, field); + writeTaggedObject(writer, value); + } + default -> throw JDWPException.raise(ErrorCode.INVALID_FIELDID); + } + } + + @Override + public Packet ObjectReference_GetValues(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Object object = readReferenceOrNull(reader); + if (object == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + + int length = reader.readInt(); + assert length >= 0; + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeInt(length); + + for (int i = 0; i < length; i++) { + InterpreterResolvedJavaField field = readField(reader); + if (field.isStatic() || !InterpreterToVM.instanceOf(object, field.getDeclaringClass())) { + // Field is static or not present in the given object. + throw JDWPException.raise(ErrorCode.INVALID_FIELDID); + } + sharedReadField(writer, object, field); + } + + assert reader.isEndOfInput(); + + return reply; + } + + @Override + public Packet ReferenceType_GetValues(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + int length = reader.readInt(); + assert length >= 0; + + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeInt(length); + + for (int i = 0; i < length; i++) { + InterpreterResolvedJavaField field = readField(reader); + if (!field.isStatic() || + !field.getDeclaringClass().getJavaClass().isAssignableFrom(type.getJavaClass())) { + // Instance field or field is not included in superclasses, superinterfaces, or + // implemented interfaces. + throw JDWPException.raise(ErrorCode.INVALID_FIELDID); + } + sharedReadField(writer, type, field); + } + + assert reader.isEndOfInput(); + + return reply; + } + + @Override + public Packet ClassLoaderReference_VisibleClasses(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + // The boot class loader (null) is accepted. + Object object = readReferenceOrNull(reader); + if (object != null && !(object instanceof ClassLoader)) { + throw JDWPException.raise(ErrorCode.INVALID_CLASS_LOADER); + } + assert reader.isEndOfInput(); + + ClassLoader classLoader = (ClassLoader) object; + List visibleTypes = classesWithInitiatingClassLoader(classLoader); + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + writer.writeInt(visibleTypes.size()); + for (ResolvedJavaType type : visibleTypes) { + writer.writeByte(TypeTag.getKind(type)); + writer.writeLong(JDWPBridgeImpl.getIds().getIdOrCreateWeak(type)); + } + + return reply; + } + + private static Class getElementalClass(Class clazz) { + Class elemental = clazz; + while (elemental.isArray()) { + elemental = elemental.getComponentType(); + } + return elemental; + } + + /** + * Returns the set of types reachable by the given class loader e.g. all classes for which the + * given class loader is an "initiating" class loader. + */ + private static List classesWithInitiatingClassLoader(ClassLoader classLoader) { + List visibleTypes = new ArrayList<>(); + // Traverse all types in the universe and check if the class is reachable + // by name, without resolution; this can be slow. + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + for (ResolvedJavaType type : universe.getTypes()) { + if (type.isPrimitive()) { + continue; + } + Class javaClass = ((InterpreterResolvedJavaType) type).getJavaClass(); + // On SVM, some classes may not be loaded yet, the Class instance is present but + // doesn't have a class loader associated with it yet. + if (!DynamicHub.fromClass(javaClass).isLoaded()) { + continue; + } + // Do not include hidden classes or interfaces or array classes whose element type is a + // hidden class or interface as they cannot be discovered by any class loader. + if (getElementalClass(javaClass).isHidden()) { + continue; + } + + ClassLoader bootLoader = null; + ClassLoader platformLoader = ClassLoader.getPlatformClassLoader(); + ClassLoader appLoader = ClassLoader.getSystemClassLoader(); + + ClassLoader loader = javaClass.getClassLoader(); + + // TODO(peterssen): GR-55067 SVM's ClassLoader#findLoadedClass doesn't respect + // class-loader hierarchy. + // SVM's ClassLoader#findLoadedClass is out-of spec e.g. the platform + // class loader can "find" classes defined by the application class loader. + // This is an attempt to fix the issue for known class loaders. + // These checks enforce the following class loader delegation hierarchy: + // System/application -> Platform -> Boot (null). + boolean isClassVisible = false; + if (classLoader == bootLoader) { + isClassVisible = (loader == bootLoader); + } else if (classLoader == platformLoader) { + isClassVisible = (loader == bootLoader || loader == platformLoader); + } else if (classLoader == appLoader) { + isClassVisible = (loader == bootLoader || loader == platformLoader || loader == appLoader); + } else { + // SVM equivalent to ClassLoader#findLoadedClass. + Class forNameClass = ClassForNameSupport.forNameOrNull(type.toClassName(), classLoader); + if (javaClass == forNameClass) { + isClassVisible = true; + } + } + + if (isClassVisible) { + visibleTypes.add(type); + } + } + + return visibleTypes; + } + + private static void readLocalFromCompiledFrame(byte tag, FrameInfoQueryResult frame, Pointer stackPointer, int slot, Packet.Writer writer) throws JDWPException { + if (!(slot >= 0 && slot <= frame.getNumLocals())) { + throw JDWPException.raise(ErrorCode.INVALID_SLOT); + } + + if (frame.getValueInfos() == null) { + /* missing locals info for this method */ + // ABSENT_INFORMATION not expected for this command. + // IDE's should deal with the error code, reporting that the information is missing + // is better than reporting nothing at all. + throw JDWPException.raise(ErrorCode.ABSENT_INFORMATION); + } + + IsolateThread targetThread = WordFactory.zero(); + DeoptState deoptState = new DeoptState(stackPointer, targetThread); + JavaConstant javaConstant = deoptState.readLocalVariable(slot, frame); + + if (javaConstant.getJavaKind().equals(JavaKind.Illegal)) { + /* either value was not encoded or it is unknown */ + // ABSENT_INFORMATION not expected for this command. + // IDE's should deal with the error code, reporting that the information is missing + // is better than reporting nothing at all. + throw JDWPException.raise(ErrorCode.ABSENT_INFORMATION); + } else { + switch (tag) { + case TagConstants.BYTE -> { + expectKind(javaConstant, JavaKind.Byte.getStackKind()); + writer.writeByte(TagConstants.BYTE); + writer.writeByte((byte) javaConstant.asInt()); + } + case TagConstants.BOOLEAN -> { + expectKind(javaConstant, JavaKind.Boolean.getStackKind()); + writer.writeByte(TagConstants.BOOLEAN); + writer.writeBoolean(javaConstant.asInt() != 0); + } + case TagConstants.SHORT -> { + expectKind(javaConstant, JavaKind.Short.getStackKind()); + writer.writeByte(TagConstants.SHORT); + writer.writeShort((short) javaConstant.asInt()); + } + case TagConstants.CHAR -> { + expectKind(javaConstant, JavaKind.Char.getStackKind()); + writer.writeByte(TagConstants.CHAR); + writer.writeChar((char) javaConstant.asInt()); + } + case TagConstants.INT -> { + expectKind(javaConstant, JavaKind.Int); + writer.writeByte(TagConstants.INT); + writer.writeInt(javaConstant.asInt()); + } + case TagConstants.LONG -> { + expectKind(javaConstant, JavaKind.Long); + writer.writeByte(TagConstants.LONG); + writer.writeLong(javaConstant.asLong()); + } + case TagConstants.FLOAT -> { + expectKind(javaConstant, JavaKind.Float); + writer.writeByte(TagConstants.FLOAT); + writer.writeFloat(javaConstant.asFloat()); + } + case TagConstants.DOUBLE -> { + expectKind(javaConstant, JavaKind.Double); + writer.writeByte(TagConstants.DOUBLE); + writer.writeDouble(javaConstant.asDouble()); + } + case TagConstants.VOID -> { + // Should this be unreachable instead? + writer.writeByte(TagConstants.VOID); + } + default -> { + expectKind(javaConstant, JavaKind.Object); + Object value = SubstrateObjectConstant.asObject(javaConstant); + byte valueTag = TagConstants.getTagFromReference(value); + // Value tag overrides provided tag. + writer.writeByte(valueTag); + writer.writeLong(JDWPBridgeImpl.getIds().getIdOrCreateWeak(value)); + } + } + } + } + + private static void expectKind(JavaConstant jc, JavaKind expectedKind) { + JavaKind givenKind = jc.getJavaKind(); + if (givenKind != expectedKind) { + throw JDWPException.raise(ErrorCode.INVALID_TAG); + } + } + + private static void readLocalFromInterpreterFrame(byte tag, InterpreterFrameSourceInfo interpreterJavaFrameInfo, int slot, Packet.Writer writer) throws JDWPException { + LocalVariableTable localVariableTable = interpreterJavaFrameInfo.getInterpretedMethod().getLocalVariableTable(); + /* + * Even if local variable information is not available, values can be retrieved if the + * front-end is able to determine the correct local variable index. Typically, this index + * can be determined for method arguments from the method signature without access to the + * local variable table information. + */ + if (localVariableTable != null) { + Local local = localVariableTable.getLocal(slot, interpreterJavaFrameInfo.getBci()); + if (local == null) { + throw JDWPException.raise(ErrorCode.INVALID_SLOT); + } + JavaKind localKind = local.getType().getJavaKind(); + JavaKind tagKind = TagConstants.tagToKind(tag); + if (localKind != tagKind) { + throw JDWPException.raise(ErrorCode.INVALID_TAG); + } + } + InterpreterFrame interpreterFrame = (InterpreterFrame) interpreterJavaFrameInfo.getInterpreterFrame(); + switch (tag) { + case TagConstants.BYTE -> { + int value = EspressoFrame.getLocalInt(interpreterFrame, slot); + writer.writeByte(TagConstants.BYTE); + writer.writeByte((byte) value); + } + case TagConstants.BOOLEAN -> { + int value = EspressoFrame.getLocalInt(interpreterFrame, slot); + writer.writeByte(TagConstants.BOOLEAN); + writer.writeBoolean(value != 0); + } + case TagConstants.SHORT -> { + int value = EspressoFrame.getLocalInt(interpreterFrame, slot); + writer.writeByte(TagConstants.SHORT); + writer.writeShort((short) value); + } + case TagConstants.CHAR -> { + int value = EspressoFrame.getLocalInt(interpreterFrame, slot); + writer.writeByte(TagConstants.CHAR); + writer.writeChar((char) value); + } + case TagConstants.INT -> { + int value = EspressoFrame.getLocalInt(interpreterFrame, slot); + writer.writeByte(TagConstants.INT); + writer.writeInt(value); + } + case TagConstants.LONG -> { + long value = EspressoFrame.getLocalLong(interpreterFrame, slot); + writer.writeByte(TagConstants.LONG); + writer.writeLong(value); + } + case TagConstants.FLOAT -> { + float value = EspressoFrame.getLocalFloat(interpreterFrame, slot); + writer.writeByte(TagConstants.FLOAT); + writer.writeFloat(value); + } + case TagConstants.DOUBLE -> { + double value = EspressoFrame.getLocalDouble(interpreterFrame, slot); + writer.writeByte(TagConstants.DOUBLE); + writer.writeDouble(value); + } + case TagConstants.VOID -> { + writer.writeByte(TagConstants.VOID); + // Write nothing here. + } + default -> { + Object value = EspressoFrame.getLocalObject(interpreterFrame, slot); + writeTaggedObject(writer, value); + } + } + } + + @Override + public Packet ClassType_SetValues(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + + if (!DynamicHub.fromClass(type.getJavaClass()).isLoaded()) { + throw JDWPException.raise(ErrorCode.CLASS_NOT_PREPARED); + } + + int fieldCount = reader.readInt(); + assert fieldCount >= 0; + for (int i = 0; i < fieldCount; i++) { + InterpreterResolvedJavaField field = readField(reader); + InterpreterResolvedJavaType fieldType = field.getType(); + if (!field.isStatic()) { + throw JDWPException.raise(ErrorCode.ILLEGAL_ARGUMENT); + } + if (field.isUndefined() || fieldType.isWordType() || field.isUnmaterializedConstant()) { + throw JDWPException.raise(ErrorCode.ILLEGAL_ARGUMENT); + } + sharedWriteField(reader, type, field); + } + + assert reader.isEndOfInput(); + + // Empty response. + return WritablePacket.newReplyTo(packet); + } + + @Override + public Packet ObjectReference_SetValues(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Object receiver = readReferenceOrNull(reader); + if (receiver == null) { + throw JDWPException.raise(ErrorCode.ILLEGAL_ARGUMENT); + } + int fieldCount = reader.readInt(); + assert fieldCount >= 0; + for (int i = 0; i < fieldCount; i++) { + InterpreterResolvedJavaField field = readField(reader); + InterpreterResolvedJavaType fieldType = field.getType(); + if (field.isStatic()) { + throw JDWPException.raise(ErrorCode.ILLEGAL_ARGUMENT); + } + if (field.isUndefined() || fieldType.isWordType() || field.isUnmaterializedConstant()) { + throw JDWPException.raise(ErrorCode.ILLEGAL_ARGUMENT); + } + sharedWriteField(reader, receiver, field); + } + + assert reader.isEndOfInput(); + + // Empty response. + return WritablePacket.newReplyTo(packet); + } + + private static void sharedWriteField(Packet.Reader reader, Object typeOrReceiver, InterpreterResolvedJavaField field) { + Object receiver; + JavaKind fieldKind = field.getJavaKind(); + if (field.isStatic()) { + assert typeOrReceiver instanceof InterpreterResolvedJavaType; + // typeOrReceiver is ignored, all static fields are grouped together. + receiver = (fieldKind.isPrimitive() || field.getType().isWordType()) + ? StaticFieldsSupport.getStaticPrimitiveFields() + : StaticFieldsSupport.getStaticObjectFields(); + } else { + receiver = typeOrReceiver; + assert receiver != null; + } + + if (field.isUndefined() || field.isUnmaterializedConstant()) { + throw JDWPException.raise(ErrorCode.ILLEGAL_ARGUMENT); + } + + assert !field.isUndefined() && !field.isUnmaterializedConstant() // + : "Cannot write undefined or unmaterialized field " + field; + + if (field.getType().isWordType()) { + switch (InterpreterToVM.wordJavaKind()) { + case Int -> + InterpreterToVM.setFieldWord(WordFactory.signed(reader.readInt()), receiver, field); + case Long -> + InterpreterToVM.setFieldWord(WordFactory.signed(reader.readLong()), receiver, field); + default -> + throw VMError.shouldNotReachHere("Unexpected word kind " + InterpreterToVM.wordJavaKind()); + } + return; + } + + // @formatter:off + switch (fieldKind) { + case Boolean -> InterpreterToVM.setFieldBoolean(reader.readBoolean(), receiver, field); + case Byte -> InterpreterToVM.setFieldByte((byte) reader.readByte(), receiver, field); + case Short -> InterpreterToVM.setFieldShort(reader.readShort(), receiver, field); + case Char -> InterpreterToVM.setFieldChar(reader.readChar(), receiver, field); + case Int -> InterpreterToVM.setFieldInt(reader.readInt(), receiver, field); + case Float -> InterpreterToVM.setFieldFloat(reader.readFloat(), receiver, field); + case Long -> InterpreterToVM.setFieldLong(reader.readLong(), receiver, field); + case Double -> InterpreterToVM.setFieldDouble(reader.readDouble(), receiver, field); + case Object -> { + assert !field.getType().isWordType() : field; // handled above + Object value = readReferenceOrNull(reader); + if (value != null && !field.getType().getJavaClass().isInstance(value)) { + throw JDWPException.raise(ErrorCode.TYPE_MISMATCH); + } + InterpreterToVM.setFieldObject(value, receiver, field); + } + default -> throw JDWPException.raise(ErrorCode.INVALID_FIELDID); + } + // @formatter:on + } + + @Override + public Packet StackFrame_SetValues(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Thread thread = readThread(reader); + assert !thread.isVirtual(); + + long frameId = reader.readLong(); + int frameDepth = FrameId.getFrameDepth(frameId); + assert frameDepth >= 0; + + // The number of values to set. + int slotValues = reader.readInt(); + assert slotValues >= 0; + + StackframeDescriptor stackframeDescriptor = getStackframeDescriptor(thread, frameDepth); + FrameSourceInfo frameSourceInfo = stackframeDescriptor.getFrameSourceInfo(); + require(frameSourceInfo != null, ErrorCode.INVALID_FRAMEID, + "Frame depth %s not found for thread.threadId()=%s", frameDepth, thread.threadId()); + + if (!(frameSourceInfo instanceof InterpreterFrameSourceInfo)) { + // GR-55013: Add support for writing locals in compiled frames. + throw JDWPException.raise(ErrorCode.NOT_IMPLEMENTED); + } + + for (int i = 0; i < slotValues; i++) { + // The local variable's index in the frame. + int slot = reader.readInt(); + byte tag = JDWP.readTag(reader); + InterpreterFrameSourceInfo interpreterJavaFrameInfo = (InterpreterFrameSourceInfo) frameSourceInfo; + writeLocalToInterpreterFrame(tag, interpreterJavaFrameInfo, slot, reader); + } + + assert reader.isEndOfInput(); + + // Empty response. + return WritablePacket.newReplyTo(packet); + } + + private static void writeLocalToInterpreterFrame(byte tag, InterpreterFrameSourceInfo interpreterJavaFrameInfo, int slot, Packet.Reader reader) { + LocalVariableTable localVariableTable = interpreterJavaFrameInfo.getInterpretedMethod().getLocalVariableTable(); + /* + * Even if local variable information is not available, values can be retrieved if the + * front-end is able to determine the correct local variable index. Typically, this index + * can be determined for method arguments from the method signature without access to the + * local variable table information. + */ + if (localVariableTable != null) { + Local local = localVariableTable.getLocal(slot, interpreterJavaFrameInfo.getBci()); + if (local == null) { + throw JDWPException.raise(ErrorCode.INVALID_SLOT); + } + JavaKind localKind = local.getType().getJavaKind(); + JavaKind tagKind = TagConstants.tagToKind(tag); + if (localKind != tagKind) { + throw JDWPException.raise(ErrorCode.INVALID_TAG); + } + } + InterpreterFrame interpreterFrame = (InterpreterFrame) interpreterJavaFrameInfo.getInterpreterFrame(); + // @formatter:off + switch (tag) { + case TagConstants.BYTE -> EspressoFrame.setLocalInt(interpreterFrame, slot, (byte) reader.readByte()); + case TagConstants.BOOLEAN -> EspressoFrame.setLocalInt(interpreterFrame, slot, reader.readBoolean() ? 1 : 0); + case TagConstants.SHORT -> EspressoFrame.setLocalInt(interpreterFrame, slot, reader.readShort()); + case TagConstants.CHAR -> EspressoFrame.setLocalInt(interpreterFrame, slot, reader.readChar()); + case TagConstants.INT -> EspressoFrame.setLocalInt(interpreterFrame, slot, reader.readInt()); + case TagConstants.LONG -> EspressoFrame.setLocalLong(interpreterFrame, slot, reader.readLong()); + case TagConstants.FLOAT -> EspressoFrame.setLocalFloat(interpreterFrame, slot, reader.readFloat()); + case TagConstants.DOUBLE -> EspressoFrame.setLocalDouble(interpreterFrame, slot, reader.readDouble()); + case TagConstants.VOID -> { } // nothing + default -> EspressoFrame.setLocalObject(interpreterFrame, slot, readReferenceOrNull(reader)); + } + // @formatter:on + } + + private static Object[] readArguments(Packet.Reader reader) { + int argCount = reader.readInt(); + assert argCount >= 0; + + Object[] args = new Object[argCount]; + for (int i = 0; i < argCount; i++) { + byte tag = JDWP.readTag(reader); + switch (tag) { + case TagConstants.BYTE -> args[i] = (byte) reader.readByte(); + case TagConstants.BOOLEAN -> args[i] = reader.readBoolean(); + case TagConstants.SHORT -> args[i] = reader.readShort(); + case TagConstants.CHAR -> args[i] = reader.readChar(); + case TagConstants.INT -> args[i] = reader.readInt(); + case TagConstants.LONG -> args[i] = reader.readLong(); + case TagConstants.FLOAT -> args[i] = reader.readFloat(); + case TagConstants.DOUBLE -> args[i] = reader.readDouble(); + case TagConstants.VOID -> { + // Read nothing. + } + default -> args[i] = readReferenceOrNull(reader); + } + } + + return args; + } + + record Result(Object value, Throwable throwable) { + + static Result fromValue(Object value) { + return new Result(value, null); + } + + static Result fromThrowable(Throwable throwable) { + return new Result(null, MetadataUtil.requireNonNull(throwable)); + } + + static Result ofInvoke(boolean isVirtual, InterpreterResolvedJavaMethod method, Object... args) { + try { + return fromValue(InterpreterToVM.dispatchInvocation(method, args, isVirtual, false, false, false)); + } catch (SemanticJavaException e) { + return fromThrowable(e.getCause()); + } catch (StackOverflowError | OutOfMemoryError error) { + return fromThrowable(error); + } + } + } + + private static void writeTaggedValue(Packet.Writer writer, Object value, JavaKind valueKind) { + switch (valueKind) { + case Boolean -> { + writer.writeByte(TagConstants.BOOLEAN); + writer.writeBoolean((boolean) value); + } + case Byte -> { + writer.writeByte(TagConstants.BYTE); + writer.writeByte((byte) value); + } + case Short -> { + writer.writeByte(TagConstants.SHORT); + writer.writeShort((short) value); + } + case Char -> { + writer.writeByte(TagConstants.CHAR); + writer.writeChar((char) value); + } + case Int -> { + writer.writeByte(TagConstants.INT); + writer.writeInt((int) value); + } + case Float -> { + writer.writeByte(TagConstants.FLOAT); + writer.writeFloat((float) value); + } + case Long -> { + writer.writeByte(TagConstants.LONG); + writer.writeLong((long) value); + } + case Double -> { + writer.writeByte(TagConstants.DOUBLE); + writer.writeDouble((double) value); + } + case Object -> { + writeTaggedObject(writer, value); + } + case Void -> { + writer.writeByte(TagConstants.VOID); + // write nothing + } + default -> + throw VMError.shouldNotReachHere("unexpected kind " + valueKind); + } + } + + /** + * Ensures that a given condition is true, throwing a {@link JDWPException} with the provided + * {@link ErrorCode error code} otherwise. Before throwing the {@link JDWPException exception}, + * the message is {@link Logger#log(String, Object...) logged}. + * + * @param logMessageSimpleFormat a "simple" format string + * @param args arguments referenced by the "simple" format string + */ + private static void require(boolean condition, ErrorCode errorCode, String logMessageSimpleFormat, Object... args) throws JDWPException { + if (!condition) { + LOGGER.log(logMessageSimpleFormat, args); + throw JDWPException.raise(errorCode); + } + } + + /** + * Ensures that a given condition is true, throwing a {@link JDWPException} with the provided + * {@link ErrorCode error code} otherwise. Before throwing the {@link JDWPException exception}, + * the {@link ErrorCode#getMessage() error message} associated with the {@link ErrorCode error + * code} is {@link Logger#log(String) logged}. + */ + @SuppressWarnings("unused") + private static void require(boolean condition, ErrorCode errorCode) throws JDWPException { + if (!condition) { + throw JDWPException.raise(errorCode); + } + } + + static Packet invokeReply(Packet packet, Result invokeResult, JavaKind returnValueKind) { + return invokeReply(packet, invokeResult.value(), invokeResult.throwable(), returnValueKind); + } + + static Packet invokeReply(Packet packet, Object value, Throwable throwable, JavaKind returnValueKind) { + WritablePacket reply = WritablePacket.newReplyTo(packet); + Packet.Writer writer = reply.dataWriter(); + /* + * The JDWP spec states that both, the receiver and the exception must be written. If an + * exception was thrown, write a 'null' return value. + * + * In case of exception, cannot reply with a return value of type/tag void since the vanilla + * JDI implementation expects the ClassType.NewInstance command to always return a value of + * type/tag object, regardless of exceptions and that methods actually returns void. + */ + if (throwable != null) { + writeTaggedObject(writer, null); + } else { + writeTaggedValue(writer, value, returnValueKind); + } + writeTaggedObject(writer, throwable); + return reply; + } + + @Override + public Packet ClassType_InvokeMethod(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + Thread thread = readThread(reader); + InterpreterResolvedJavaMethod method = readMethod(reader); + Object[] args = readArguments(reader); + @SuppressWarnings("unused") + int options = reader.readInt(); + assert reader.isEndOfInput(); + + require(thread == Thread.currentThread(), ErrorCode.ILLEGAL_ARGUMENT, "method invocation only supports current/same thread"); + require(method.isStatic(), ErrorCode.ILLEGAL_ARGUMENT, "method must be static %s", method); + require(type.equals(method.getDeclaringClass()), ErrorCode.ILLEGAL_ARGUMENT, "method declaring type %s and type %s differ", method.getDeclaringClass(), type); + require(!thread.isVirtual(), ErrorCode.ILLEGAL_ARGUMENT, "virtual threads not supported"); + // InvokeOptions.INVOKE_NONVIRTUAL is ignored. + + return invokeReply(packet, Result.ofInvoke(false, method, args), method.getSignature().getReturnKind()); + } + + @Override + public Packet InterfaceType_InvokeMethod(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + Thread thread = readThread(reader); + InterpreterResolvedJavaMethod method = readMethod(reader); + Object[] args = readArguments(reader); + @SuppressWarnings("unused") + int options = reader.readInt(); + assert reader.isEndOfInput(); + + require(!method.isClassInitializer(), ErrorCode.ILLEGAL_ARGUMENT, "method cannot be a static initializer %s", method); + require(method.isStatic(), ErrorCode.ILLEGAL_ARGUMENT, "method must be be static %s", method); + require(type.equals(method.getDeclaringClass()), ErrorCode.ILLEGAL_ARGUMENT, "method declaring type %s and type %s differ", method.getDeclaringClass(), type); + require(type.isInterface(), ErrorCode.ILLEGAL_ARGUMENT, "type %s is not an interface"); + require(type.equals(method.getDeclaringClass()), ErrorCode.ILLEGAL_ARGUMENT, "method %s is not a member of the interface type %s", method, type); + require(!thread.isVirtual(), ErrorCode.ILLEGAL_ARGUMENT, "virtual threads not supported"); + // InvokeOptions.INVOKE_NONVIRTUAL is ignored. + + return invokeReply(packet, Result.ofInvoke(false, method, args), method.getSignature().getReturnKind()); + } + + @Override + public Packet ObjectReference_InvokeMethod(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + Object receiver = readReferenceOrNull(reader); + Thread thread = readThread(reader); + @SuppressWarnings("unused") + InterpreterResolvedJavaType type = readType(reader); + InterpreterResolvedJavaMethod method = readMethod(reader); + Object[] argsWithoutReceiver = readArguments(reader); + int options = reader.readInt(); + assert reader.isEndOfInput(); + + require(receiver != null, ErrorCode.ILLEGAL_ARGUMENT, "receiver is null"); + require(!method.isStatic(), ErrorCode.ILLEGAL_ARGUMENT, "method cannot be static %s", method); + require(method.getDeclaringClass().isAssignableFrom(type), ErrorCode.ILLEGAL_ARGUMENT, + "method %s is not declared in type %s nor any of its super types (or super interfaces)", method, type); + require(method.getDeclaringClass().getJavaClass().isInstance(receiver), ErrorCode.ILLEGAL_ARGUMENT, + "method %s is not declared in the receiver type %s nor any of its super types (or super interfaces)", method, receiver.getClass()); + require(!thread.isVirtual(), ErrorCode.ILLEGAL_ARGUMENT, "virtual threads not supported"); + + Object[] args = prepend(receiver, argsWithoutReceiver); + boolean isVirtual = !InvokeOptions.nonVirtual(options); + return invokeReply(packet, Result.ofInvoke(isVirtual, method, args), method.getSignature().getReturnKind()); + } + + @Override + public Packet ClassType_NewInstance(Packet packet) throws JDWPException { + Packet.Reader reader = packet.newDataReader(); + InterpreterResolvedJavaType type = readType(reader); + Thread thread = readThread(reader); + InterpreterResolvedJavaMethod method = readMethod(reader); + Object[] argsWithoutReceiver = readArguments(reader); + @SuppressWarnings("unused") + int options = reader.readInt(); + assert reader.isEndOfInput(); + + require(!type.isPrimitive(), ErrorCode.ILLEGAL_ARGUMENT, "invalid primitive type %s", type); + require(!type.isArray(), ErrorCode.ILLEGAL_ARGUMENT, "invalid array type %s", type); + require(!type.isAbstract(), ErrorCode.ILLEGAL_ARGUMENT, "invalid abstract type %s", type); + require(method.isConstructor(), ErrorCode.ILLEGAL_ARGUMENT, "method is not a constructor %s", method); + require(!method.isStatic(), ErrorCode.ILLEGAL_ARGUMENT, "constructor cannot be static %s", method); + require(type.equals(method.getDeclaringClass()), ErrorCode.ILLEGAL_ARGUMENT, "constructor %s is not a member of the given type %s", method, type); + require(!thread.isVirtual(), ErrorCode.ILLEGAL_ARGUMENT, "virtual threads not supported"); + + Object instance; + try { + instance = InterpreterToVM.createNewReference(type); + assert instance != null; + } catch (SemanticJavaException e) { + return invokeReply(packet, null, e.getCause(), JavaKind.Object); + } + + Object[] args = prepend(instance, argsWithoutReceiver); + return invokeReply(packet, instance, Result.ofInvoke(false, method, args).throwable(), JavaKind.Object); + } + + private static Object[] prepend(Object newFirst, Object[] array) { + Object[] newArray = new Object[array.length + 1]; + newArray[0] = newFirst; + System.arraycopy(array, 0, newArray, 1, array.length); + return newArray; + } + + @Override + public Packet dispatch(Packet packet) throws JDWPException { + try { + return JDWP.super.dispatch(packet); + } catch (JDWPException e) { + ResidentJDWP.LOGGER.log(e, "JDWP exception"); + throw e; + } catch (Throwable t) { + ResidentJDWP.LOGGER.log(t, "Internal error"); + throw t; + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/ResidentSymbolicRefs.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/ResidentSymbolicRefs.java new file mode 100644 index 000000000000..7ffd2a315a4a --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/ResidentSymbolicRefs.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident.impl; + +import java.util.OptionalInt; + +import com.oracle.svm.interpreter.DebuggerSupport; +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; +import com.oracle.svm.jdwp.bridge.ErrorCode; +import com.oracle.svm.jdwp.bridge.JDWPException; +import com.oracle.svm.jdwp.bridge.SymbolicRefs; +import com.oracle.svm.jdwp.resident.JDWPBridgeImpl; + +import jdk.vm.ci.meta.ResolvedJavaField; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +public final class ResidentSymbolicRefs implements SymbolicRefs { + + @Override + public long toTypeRef(ResolvedJavaType resolvedJavaType) { + if (resolvedJavaType == null) { + return 0L; + } + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt typeIndexFor = universe.getTypeIndexFor(resolvedJavaType); + int typeIndex = typeIndexFor.orElseThrow(IllegalArgumentException::new); + return JDWPBridgeImpl.getIds().getIdOrCreateWeak(universe.getTypeAtIndex(typeIndex)); + } + + @Override + public long toFieldRef(ResolvedJavaField resolvedJavaField) { + if (resolvedJavaField == null) { + return 0L; + } + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt fieldIndexFor = universe.getFieldIndexFor(resolvedJavaField); + int fieldIndex = fieldIndexFor.orElseThrow(IllegalArgumentException::new); + return JDWPBridgeImpl.getIds().getIdOrCreateWeak(universe.getFieldAtIndex(fieldIndex)); + } + + @Override + public long toMethodRef(ResolvedJavaMethod resolvedJavaMethod) { + if (resolvedJavaMethod == null) { + return 0L; + } + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt methodIndexFor = universe.getMethodIndexFor(resolvedJavaMethod); + int methodIndex = methodIndexFor.orElseThrow(IllegalArgumentException::new); + return JDWPBridgeImpl.getIds().getIdOrCreateWeak(universe.getMethodAtIndex(methodIndex)); + } + + @Override + public ResolvedJavaType toResolvedJavaType(long typeRefId) throws JDWPException { + if (typeRefId == 0) { + return null; + } + ResolvedJavaType resolvedJavaType; + try { + resolvedJavaType = JDWPBridgeImpl.getIds().toObject(typeRefId, ResolvedJavaType.class); + } catch (ClassCastException e) { + throw JDWPException.raise(ErrorCode.INVALID_CLASS); + } + if (resolvedJavaType == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt typeIndexFor = universe.getTypeIndexFor(resolvedJavaType); + if (typeIndexFor.isEmpty()) { + throw JDWPException.raise(ErrorCode.INVALID_CLASS); + } + return resolvedJavaType; + } + + @Override + public ResolvedJavaField toResolvedJavaField(long fieldRefId) throws JDWPException { + if (fieldRefId == 0) { + return null; + } + ResolvedJavaField resolvedJavaField; + try { + resolvedJavaField = JDWPBridgeImpl.getIds().toObject(fieldRefId, ResolvedJavaField.class); + } catch (ClassCastException e) { + throw JDWPException.raise(ErrorCode.INVALID_FIELDID); + } + if (resolvedJavaField == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt typeIndexFor = universe.getFieldIndexFor(resolvedJavaField); + if (typeIndexFor.isEmpty()) { + throw JDWPException.raise(ErrorCode.INVALID_FIELDID); + } + return resolvedJavaField; + } + + @Override + public ResolvedJavaMethod toResolvedJavaMethod(long methodRefId) throws JDWPException { + if (methodRefId == 0) { + return null; + } + ResolvedJavaMethod resolvedJavaMethod; + try { + resolvedJavaMethod = JDWPBridgeImpl.getIds().toObject(methodRefId, ResolvedJavaMethod.class); + } catch (ClassCastException e) { + throw JDWPException.raise(ErrorCode.INVALID_METHODID); + } + if (resolvedJavaMethod == null) { + throw JDWPException.raise(ErrorCode.INVALID_OBJECT); + } + InterpreterUniverse universe = DebuggerSupport.singleton().getUniverse(); + OptionalInt methodIndexFor = universe.getMethodIndexFor(resolvedJavaMethod); + if (methodIndexFor.isEmpty()) { + throw JDWPException.raise(ErrorCode.INVALID_METHODID); + } + return resolvedJavaMethod; + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/SafeStackWalker.java b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/SafeStackWalker.java new file mode 100644 index 000000000000..5c147bf500d9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.resident/src/com/oracle/svm/jdwp/resident/impl/SafeStackWalker.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.resident.impl; + +import com.oracle.svm.core.NeverInline; +import com.oracle.svm.core.heap.VMOperationInfos; +import com.oracle.svm.core.jdk.InternalVMMethod; +import com.oracle.svm.core.snippets.KnownIntrinsics; +import com.oracle.svm.core.stack.JavaStackWalker; +import com.oracle.svm.core.stack.StackFrameVisitor; +import com.oracle.svm.core.thread.JavaVMOperation; +import com.oracle.svm.core.thread.PlatformThreads; +import org.graalvm.nativeimage.IsolateThread; + +public final class SafeStackWalker { + @NeverInline("Starting a stack walk in the caller frame") + public static void safeStackWalk(Thread targetThread, StackFrameVisitor visitor) { + assert targetThread != null; + if (targetThread == Thread.currentThread()) { + JavaStackWalker.walkCurrentThread(KnownIntrinsics.readCallerStackPointer(), visitor); + } else { + // Stack-walking another (suspended) thread requires a safepoint. + StackWalkOperation stackWalkOp = new StackWalkOperation(targetThread, visitor); + stackWalkOp.enqueue(); + } + } + + @InternalVMMethod + private static final class StackWalkOperation extends JavaVMOperation { + private final Thread thread; + private final StackFrameVisitor visitor; + + StackWalkOperation(Thread thread, StackFrameVisitor visitor) { + super(VMOperationInfos.get(StackWalkOperation.class, "Stack walking", SystemEffect.SAFEPOINT)); + this.thread = thread; + this.visitor = visitor; + } + + @Override + @NeverInline("Starting a stack walk.") + protected void operate() { + assert thread.isAlive(); + IsolateThread isolateThread = PlatformThreads.getIsolateThreadUnsafe(thread); + JavaStackWalker.walkThread(isolateThread, visitor); + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/ClassUtils.java b/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/ClassUtils.java new file mode 100644 index 000000000000..7f524f188218 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/ClassUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.server; + +import com.oracle.svm.interpreter.metadata.InterpreterUniverse; + +/** + * Utility methods for ResolvedJavaType. + */ +public final class ClassUtils { + + public static InterpreterUniverse UNIVERSE = null; + + private ClassUtils() { + throw new UnsupportedOperationException("Do not instantiate"); + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/JDWPHandler.java b/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/JDWPHandler.java new file mode 100644 index 000000000000..46d8e0d4404e --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/JDWPHandler.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.server; + +import java.io.IOException; +import java.net.ConnectException; +import java.util.ArrayList; +import java.util.Collection; + +import com.oracle.svm.jdwp.bridge.DebugOptions; +import com.oracle.svm.jdwp.bridge.Packet; +import com.oracle.svm.jdwp.server.api.ConnectionController; +import com.oracle.svm.jdwp.server.impl.DebuggerConnection; +import com.oracle.svm.jdwp.server.impl.DebuggerController; +import com.oracle.svm.jdwp.server.impl.HandshakeController; +import com.oracle.svm.jdwp.server.impl.SocketConnection; + +public final class JDWPHandler implements Runnable { + + static final String HOST = "*"; + static final int PORT = 8000; + + private final ConnectionControllerImpl connectionController; + private volatile DebuggerController debuggerController; + + JDWPHandler(long initialThreadId) { + connectionController = new ConnectionControllerImpl(initialThreadId); + } + + public DebuggerController getController() { + return debuggerController; + } + + void doConnect(DebugOptions.Options options) { + connectionController.doConnect(options); + } + + @Override + public void run() { + DebugOptions.Options dummyOptions = DebugOptions.parse("transport=dt_socket,server=y,suspend=n,address=" + HOST + ":" + PORT, false); + doConnect(dummyOptions); + } + + private static void handleConnectException(ConnectException ex) { + System.err.println("ERROR: transport error 202: connect failed: " + ex.getMessage()); + System.err.println("ERROR: JDWP Transport dt_socket failed to initialize, TRANSPORT_INIT(510)"); + System.err.println("JDWP exit error AGENT_ERROR_TRANSPORT_INIT(197): No transports initialized"); + } + + private final class ConnectionControllerImpl implements ConnectionController { + + private long initialThreadId; + private DebugOptions.Options lastOptions; + private volatile DebuggerConnection lastConnection; + + private ConnectionControllerImpl(long initialThreadId) { + this.initialThreadId = initialThreadId; + } + + @Override + public void dispose(Packet replyPacket) { + DebuggerConnection connection = lastConnection; + lastConnection = null; // no sync, dispose() and restart() do not run in parallel + if (connection != null) { + connection.queuePacket(replyPacket); + connection.close(); + } + } + + @Override + public void restart() { + if (lastOptions != null && lastOptions.server()) { + doConnect(lastOptions); + } + } + + void doConnect(DebugOptions.Options options) { + lastOptions = options; + + SocketConnection socketConnection; + + Collection activeThreads = new ArrayList<>(); + try (HandshakeController hsController = new HandshakeController(options)) { + socketConnection = hsController.createSocketConnection(activeThreads); + } catch (ConnectException ex) { + handleConnectException(ex); + return; + } catch (IOException ioex) { + System.err.println("Critical failure in establishing JDWP connection: " + ioex.getLocalizedMessage()); + return; + } + + // connection established with handshake. Prepare to process commands from debugger + DebuggerController controller = new DebuggerController(initialThreadId, connectionController); + debuggerController = controller; + DebuggerConnection connection = new DebuggerConnection(socketConnection, controller, activeThreads); + controller.getEventListener().setConnection(socketConnection); + // The VM started event must be sent when we're ready to process commands + // doProcessCommands method will control when events can be fired without + // causing races, so pass on a Callable + Runnable vmStartedJob = () -> controller.getEventListener().vmStarted(options.suspend()); + lastConnection = connection; + connection.doProcessCommands(options.suspend(), vmStartedJob); + initialThreadId = 0; // no initial thread for further connections + } + } +} diff --git a/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/JDWPServer.java b/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/JDWPServer.java new file mode 100644 index 000000000000..1bee8c3d5c81 --- /dev/null +++ b/substratevm/src/com.oracle.svm.jdwp.server/src/com/oracle/svm/jdwp/server/JDWPServer.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.jdwp.server; + +import java.io.IOException; +import java.nio.file.Path; + +import com.oracle.svm.jdwp.bridge.nativebridge.NativeIsolate; +import org.graalvm.nativeimage.ImageInfo; +import org.graalvm.nativeimage.VMRuntime; + +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.interpreter.metadata.InterpreterUniverseImpl; +import com.oracle.svm.interpreter.metadata.serialization.SerializationContext; +import com.oracle.svm.interpreter.metadata.serialization.Serializers; +import com.oracle.svm.jdwp.bridge.DebugOptions; +import com.oracle.svm.jdwp.bridge.HSToNativeJDWPBridge; +import com.oracle.svm.jdwp.bridge.JDWPEventHandlerBridge; +import com.oracle.svm.jdwp.bridge.JDWPJNIConfig; +import com.oracle.svm.jdwp.server.impl.ServerJDWP; + +@SuppressWarnings("unused") +public class JDWPServer implements JDWPEventHandlerBridge { + + public static final String DEBUGGER_HELP_MESSAGE = """ + Native Image JDWP Debugger + -------------------------- + + (See the "VM Invocation Options" section of the JPDA + "Connection and Invocation Details" document for more information.) + + jdwp usage: foobar -XX:JDWPOptions=[help]|[