From fd1bd0be167544e4257dc99c5609b9cd42b662fa Mon Sep 17 00:00:00 2001 From: Florian Angerer Date: Tue, 23 Sep 2025 16:06:07 +0200 Subject: [PATCH] Intercept up-/downcall requests for error reporting --- substratevm/mx.substratevm/suite.py | 1 + .../config/ForeignConfiguration.java | 2 +- .../com/oracle/svm/core/foreign/AbiUtils.java | 4 +- .../core/foreign/ForeignFunctionsRuntime.java | 108 ++++++++++++++---- .../core/foreign/SubstrateForeignUtil.java | 83 ++++++++++++++ ...k_internal_foreign_abi_AbstractLinker.java | 41 ++++++- 6 files changed, 212 insertions(+), 27 deletions(-) diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index aeed2a91c2a8..4c5041303b18 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -788,6 +788,7 @@ "jdk.internal.foreign.abi.aarch64", "jdk.internal.foreign.abi.aarch64.macos", "jdk.internal.foreign.abi.aarch64.linux", + "jdk.internal.foreign.layout", "jdk.internal.loader", "jdk.internal.reflect", ], diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ForeignConfiguration.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ForeignConfiguration.java index ee87a9d1a20b..f2a6fae20155 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ForeignConfiguration.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ForeignConfiguration.java @@ -56,7 +56,7 @@ public void printJson(JsonWriter writer) throws IOException { } } - private record StubDesc(UnresolvedAccessCondition condition, ConfigurationFunctionDescriptor desc, Map linkerOptions) implements JsonPrintable { + public record StubDesc(UnresolvedAccessCondition condition, ConfigurationFunctionDescriptor desc, Map linkerOptions) implements JsonPrintable { @Override public void printJson(JsonWriter writer) throws IOException { writer.appendObjectStart(); diff --git a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/AbiUtils.java b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/AbiUtils.java index 4e5520f08f0c..317322aa4394 100644 --- a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/AbiUtils.java +++ b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/AbiUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, 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 @@ -48,7 +48,6 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Isolate; import org.graalvm.nativeimage.Platform; -import org.graalvm.nativeimage.Platform.HOSTED_ONLY; import org.graalvm.nativeimage.Platforms; import org.graalvm.word.Pointer; @@ -525,7 +524,6 @@ public static AbiUtils singleton() { * This method re-implements a part of the logic from the JDK so that we can get the callee-type * (i.e. the ABI low-level type) of a function from its descriptor. */ - @Platforms(HOSTED_ONLY.class) @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+18/src/java.base/share/classes/jdk/internal/foreign/abi/AbstractLinker.java#L99") @BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+18/src/java.base/share/classes/jdk/internal/foreign/abi/DowncallLinker.java#L71-L85") public final NativeEntryPointInfo makeNativeEntrypoint(FunctionDescriptor desc, LinkerOptions linkerOptions) { diff --git a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/ForeignFunctionsRuntime.java b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/ForeignFunctionsRuntime.java index c064ab470e92..8689f83d819f 100644 --- a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/ForeignFunctionsRuntime.java +++ b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/ForeignFunctionsRuntime.java @@ -26,16 +26,19 @@ import static jdk.graal.compiler.core.common.spi.ForeignCallDescriptor.CallSideEffect.HAS_SIDE_EFFECT; +import java.io.IOException; import java.lang.constant.DirectMethodHandleDesc; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; import java.lang.foreign.MemorySegment.Scope; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; +import java.util.Deque; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedDeque; import java.util.function.BiConsumer; import org.graalvm.collections.EconomicMap; @@ -71,6 +74,8 @@ import com.oracle.svm.core.util.VMError; import jdk.graal.compiler.api.replacements.Fold; +import jdk.graal.compiler.util.json.JsonPrintable; +import jdk.graal.compiler.util.json.JsonWriter; import jdk.graal.compiler.word.Word; import jdk.internal.foreign.CABI; import jdk.internal.foreign.MemorySessionImpl; @@ -85,6 +90,7 @@ public static ForeignFunctionsRuntime singleton() { return ImageSingletons.lookup(ForeignFunctionsRuntime.class); } + private final AbiUtils abiUtils; private final AbiUtils.TrampolineTemplate trampolineTemplate; private final EconomicMap downcallStubs = ImageHeapMap.create("downcallStubs"); @@ -93,6 +99,14 @@ public static ForeignFunctionsRuntime singleton() { private final EconomicSet neverAccessesSharedArenaTypes = EconomicSet.create(); private final EconomicSet neverAccessesSharedArenaMethods = EconomicSet.create(); + /** + * A thread-safe stack of currently performed link requests (i.e. creating a downcall handle or + * an upcall stub). This stack is used to generate a helpful error message if the link request + * fails because of a missing stub. Since link requests may be created concurrently, we need to + * use a thread-safe collection. + */ + private final Deque currentLinkRequests = new ConcurrentLinkedDeque<>(); + private final Map trampolines = new HashMap<>(); private TrampolineSet currentTrampolineSet; @@ -101,6 +115,7 @@ public static ForeignFunctionsRuntime singleton() { @Platforms(Platform.HOSTED_ONLY.class) public ForeignFunctionsRuntime(AbiUtils abiUtils) { + this.abiUtils = abiUtils; this.trampolineTemplate = new TrampolineTemplate(new byte[abiUtils.trampolineSize()]); } @@ -178,16 +193,10 @@ public void registerSafeArenaAccessorMethod(ResolvedJavaMethod method) { neverAccessesSharedArenaMethods.add(method); } - /** - * We'd rather report the function descriptor than the native method type, but we don't have it - * available here. One could intercept this exception in - * {@link jdk.internal.foreign.abi.DowncallLinker#getBoundMethodHandle} and add information - * about the descriptor there. - */ CFunctionPointer getDowncallStubPointer(NativeEntryPointInfo nep) { FunctionPointerHolder holder = downcallStubs.get(nep); if (holder == null) { - throw MissingForeignRegistrationUtils.reportDowncall(nep); + throw reportMissingDowncall(nep); } return holder.functionPointer; } @@ -195,7 +204,7 @@ CFunctionPointer getDowncallStubPointer(NativeEntryPointInfo nep) { CFunctionPointer getUpcallStubPointer(JavaEntryPointInfo jep) { FunctionPointerHolder holder = upcallStubs.get(jep); if (holder == null) { - throw MissingForeignRegistrationUtils.reportUpcall(jep); + throw reportMissingUpcall(jep); } return holder.functionPointer; } @@ -270,34 +279,91 @@ void freeTrampoline(long addr) { } } - public static class MissingForeignRegistrationUtils extends MissingRegistrationUtils { - public static MissingForeignRegistrationError reportDowncall(NativeEntryPointInfo nep) { - MissingForeignRegistrationError mfre = new MissingForeignRegistrationError(foreignRegistrationMessage("downcall", nep.methodType())); - report(mfre); - return mfre; - } - - public static MissingForeignRegistrationError reportUpcall(JavaEntryPointInfo jep) { - MissingForeignRegistrationError mfre = new MissingForeignRegistrationError(foreignRegistrationMessage("upcall", jep.cMethodType())); - report(mfre); - return mfre; + /** + * Looks for the corresponding {@link #currentLinkRequests link request} by creating a + * {@link NativeEntryPointInfo} for each currently existing link request and comparing to the + * given one. The matching link request then contains the {@link FunctionDescriptor} and + * {@link LinkerOptions} that are required to produce a helpful error message for the user. + */ + private MissingForeignRegistrationError reportMissingDowncall(NativeEntryPointInfo nep) { + LinkRequest currentLinkRequest = null; + for (LinkRequest linkRequest : currentLinkRequests) { + NativeEntryPointInfo nativeEntryPointInfo = abiUtils.makeNativeEntrypoint(linkRequest.functionDescriptor, linkRequest.linkerOptions); + if (nep.equals(nativeEntryPointInfo)) { + currentLinkRequest = linkRequest; + break; + } } + throw MissingForeignRegistrationUtils.report(false, currentLinkRequest, nep.methodType()); + } - private static String foreignRegistrationMessage(String failedAction, MethodType methodType) { - return registrationMessage("perform " + failedAction + " with leaf type", methodType.toString(), "", "", "foreign", "foreign"); + /** + * Similar to {@link #reportMissingDowncall} but for upcalls. + */ + private MissingForeignRegistrationError reportMissingUpcall(JavaEntryPointInfo jep) { + LinkRequest currentLinkRequest = null; + for (LinkRequest linkRequest : currentLinkRequests) { + JavaEntryPointInfo javaEntryPointInfo = abiUtils.makeJavaEntryPoint(linkRequest.functionDescriptor, linkRequest.linkerOptions); + if (jep.equals(javaEntryPointInfo)) { + currentLinkRequest = linkRequest; + break; + } } + throw MissingForeignRegistrationUtils.report(true, currentLinkRequest, jep.handleType()); + } + public static class MissingForeignRegistrationUtils extends MissingRegistrationUtils { private static void report(MissingForeignRegistrationError exception) { StackTraceElement responsibleClass = getResponsibleClass(exception, foreignEntryPoints); MissingRegistrationUtils.report(exception, responsibleClass); } + private static MissingForeignRegistrationError report(boolean upcall, LinkRequest linkRequest, MethodType methodType) { + String json = linkRequest != null ? elementToJSON(linkRequest) : ""; + String failedAction = upcall ? "upcall" : "downcall"; + String message = registrationMessage("perform " + failedAction + " with leaf type", methodType.toString(), json, "", "foreign", "foreign-function-and-memory-api"); + MissingForeignRegistrationError mfre = new MissingForeignRegistrationError(message); + report(mfre); + throw mfre; + } + private static final Map> foreignEntryPoints = Map.of( "jdk.internal.foreign.abi.AbstractLinker", Set.of( "downcallHandle", "upcallStub")); } + record LinkRequest(boolean upcall, FunctionDescriptor functionDescriptor, LinkerOptions linkerOptions) implements AutoCloseable, JsonPrintable { + + static LinkRequest create(boolean upcall, FunctionDescriptor functionDescriptor, LinkerOptions linkerOptions) { + LinkRequest linkRequest = new LinkRequest(upcall, functionDescriptor, linkerOptions); + ForeignFunctionsRuntime.singleton().currentLinkRequests.push(linkRequest); + return linkRequest; + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public void close() { + ForeignFunctionsRuntime.singleton().currentLinkRequests.remove(this); + } + + @Override + public void printJson(JsonWriter writer) throws IOException { + writer.printValue(upcall ? "upcalls" : "downcalls").appendFieldSeparator().appendArrayStart(); + SubstrateForeignUtil.linkRequestToJsonPrintable(this).printJson(writer); + writer.appendArrayEnd(); + } + } + /** * Arguments follow the same structure as described in {@link NativeEntryPointInfo}, with an * additional {@link Target_jdk_internal_foreign_abi_NativeEntryPoint} (NEP) as the last diff --git a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/SubstrateForeignUtil.java b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/SubstrateForeignUtil.java index 960442874d7e..114ed2717a65 100644 --- a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/SubstrateForeignUtil.java +++ b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/SubstrateForeignUtil.java @@ -24,9 +24,26 @@ */ package com.oracle.svm.core.foreign; +import java.lang.foreign.GroupLayout; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.PaddingLayout; +import java.lang.foreign.SequenceLayout; +import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.oracle.svm.configure.UnresolvedAccessCondition; +import com.oracle.svm.configure.config.ForeignConfiguration.ConfigurationFunctionDescriptor; +import com.oracle.svm.configure.config.ForeignConfiguration.StubDesc; import com.oracle.svm.core.AlwaysInline; import com.oracle.svm.core.ArenaIntrinsics; import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.foreign.ForeignFunctionsRuntime.LinkRequest; import com.oracle.svm.core.nodes.foreign.ScopedMemExceptionHandlerClusterNode.ClusterBeginNode; import com.oracle.svm.core.nodes.foreign.ScopedMemExceptionHandlerClusterNode.ExceptionInputNode; import com.oracle.svm.core.nodes.foreign.ScopedMemExceptionHandlerClusterNode.ExceptionPathNode; @@ -35,7 +52,10 @@ import com.oracle.svm.util.LogUtils; import jdk.graal.compiler.api.directives.GraalDirectives; +import jdk.graal.compiler.util.json.JsonPrintable; import jdk.internal.foreign.MemorySessionImpl; +import jdk.internal.foreign.abi.LinkerOptions; +import jdk.internal.foreign.layout.AbstractLayout; /** * For details on the implementation of shared arenas on substrate see @@ -119,4 +139,67 @@ public static void sessionExceptionHandler(MemorySessionImpl session, Object bas // of control flow when searching for this pattern GraalDirectives.controlFlowAnchor(); } + + /** + * Generates a JSON configuration entry that will specify the stub required by the given link + * request. This method is meant to be used to generate a useful error message for the user if a + * stub lookup failed. + */ + static JsonPrintable linkRequestToJsonPrintable(LinkRequest linkRequest) { + List parameterTypes = new LinkedList<>(); + for (MemoryLayout argumentLayout : linkRequest.functionDescriptor().argumentLayouts()) { + parameterTypes.add(memoryLayoutToString(argumentLayout)); + } + Optional memoryLayout = linkRequest.functionDescriptor().returnLayout(); + String returnLayout = memoryLayout.map(SubstrateForeignUtil::memoryLayoutToString).orElse("void"); + ConfigurationFunctionDescriptor desc = new ConfigurationFunctionDescriptor(returnLayout, parameterTypes); + + Map jsonOptions = Map.of(); + if (!LinkerOptions.empty().equals(linkRequest.linkerOptions())) { + jsonOptions = new HashMap<>(); + if (linkRequest.linkerOptions().isCritical()) { + jsonOptions.put("critical", Map.of("allowHeapAccess", linkRequest.linkerOptions().allowsHeapAccess())); + } + if (linkRequest.linkerOptions().isVariadicFunction()) { + jsonOptions.put("firstVariadicArg", linkRequest.linkerOptions().firstVariadicArgIndex()); + } + if (linkRequest.linkerOptions().hasCapturedCallState()) { + jsonOptions.put("captureCallState", true); + } + } + + return new StubDesc(UnresolvedAccessCondition.unconditional(), desc, jsonOptions); + } + + /** + * Generates a memory layout description as defined in {@code FFM-API.md} to be used in the + * configuration file (usually {@code reachability-metadata.json}). This method implements the + * reverse operation of {@code com.oracle.svm.hosted.foreign.MemoryLayoutParser}. + */ + private static String memoryLayoutToString(MemoryLayout memoryLayout) { + String layoutString = switch (memoryLayout) { + case ValueLayout valueLayout -> { + Class carrier = valueLayout.carrier(); + if (carrier == MemorySegment.class) { + yield "void*"; + } + yield "j" + carrier.getName(); + } + case GroupLayout structLayout -> { + String[] memberStrings = new String[structLayout.memberLayouts().size()]; + int i = 0; + for (MemoryLayout memberLayout : structLayout.memberLayouts()) { + memberStrings[i++] = memoryLayoutToString(memberLayout); + } + String prefix = structLayout instanceof StructLayout ? "struct(" : "union("; + yield prefix + String.join(",", memberStrings) + ")"; + } + case SequenceLayout sequenceLayout -> "sequence(" + sequenceLayout.elementCount() + ", " + memoryLayoutToString(sequenceLayout.elementLayout()) + ")"; + case PaddingLayout paddingLayout -> "padding(" + paddingLayout.byteSize() + ")"; + }; + if (memoryLayout instanceof AbstractLayout abstractLayout && !abstractLayout.hasNaturalAlignment()) { + return "align(" + memoryLayout.byteAlignment() + ", " + layoutString + ")"; + } + return layoutString; + } } diff --git a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_AbstractLinker.java b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_AbstractLinker.java index 192d94011180..97573b4b80c2 100644 --- a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_AbstractLinker.java +++ b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_AbstractLinker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, 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 @@ -37,6 +37,7 @@ import com.oracle.svm.core.annotate.RecomputeFieldValue; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.foreign.ForeignFunctionsRuntime.LinkRequest; import jdk.internal.foreign.abi.AbstractLinker; import jdk.internal.foreign.abi.AbstractLinker.UpcallStubFactory; @@ -73,9 +74,13 @@ final class Target_jdk_internal_foreign_abi_SoftReferenceCache { */ record UpcallStubFactoryDecorator(UpcallStubFactory delegate, FunctionDescriptor function, LinkerOptions options) implements UpcallStubFactory { + @SuppressWarnings({"try", "unused"}) @Override public MemorySegment makeStub(MethodHandle target, Arena arena) { - MemorySegment segment = delegate.makeStub(target, arena); + MemorySegment segment; + try (LinkRequest ignore = LinkRequest.create(true, function, options)) { + segment = delegate.makeStub(target, arena); + } /* * We cannot do this in 'UpcallLinker.makeUpcallStub' because that one already gets a @@ -95,6 +100,14 @@ public MemorySegment makeStub(MethodHandle target, Arena arena) { @TargetClass(value = SysVx64Linker.class, onlyWith = ForeignAPIPredicates.FunctionCallsSupported.class) final class Target_jdk_internal_foreign_abi_x64_sysv_SysVx64Linker { + @SuppressWarnings({"static-method", "try", "unused"}) + @Substitute + MethodHandle arrangeDowncall(MethodType inferredMethodType, FunctionDescriptor function, LinkerOptions options) { + try (LinkRequest ignore = LinkRequest.create(false, function, options)) { + return jdk.internal.foreign.abi.x64.sysv.CallArranger.arrangeDowncall(inferredMethodType, function, options); + } + } + @SuppressWarnings("static-method") @Substitute UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor function, LinkerOptions options) { @@ -105,6 +118,14 @@ UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor functi @TargetClass(value = Windowsx64Linker.class, onlyWith = ForeignAPIPredicates.FunctionCallsSupported.class) final class Target_jdk_internal_foreign_abi_x64_windows_Windowsx64Linker { + @SuppressWarnings({"static-method", "try", "unused"}) + @Substitute + MethodHandle arrangeDowncall(MethodType inferredMethodType, FunctionDescriptor function, LinkerOptions options) { + try (LinkRequest ignore = LinkRequest.create(false, function, options)) { + return jdk.internal.foreign.abi.x64.windows.CallArranger.arrangeDowncall(inferredMethodType, function, options); + } + } + @SuppressWarnings("static-method") @Substitute UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor function, LinkerOptions options) { @@ -115,6 +136,14 @@ UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor functi @TargetClass(value = MacOsAArch64Linker.class, onlyWith = ForeignAPIPredicates.FunctionCallsSupported.class) final class Target_jdk_internal_foreign_abi_aarch64_macos_MacOsAArch64Linker { + @SuppressWarnings({"static-method", "try", "unused"}) + @Substitute + MethodHandle arrangeDowncall(MethodType inferredMethodType, FunctionDescriptor function, LinkerOptions options) { + try (LinkRequest ignore = LinkRequest.create(false, function, options)) { + return jdk.internal.foreign.abi.aarch64.CallArranger.MACOS.arrangeDowncall(inferredMethodType, function, options); + } + } + @SuppressWarnings("static-method") @Substitute UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor function, LinkerOptions options) { @@ -125,6 +154,14 @@ UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor functi @TargetClass(value = LinuxAArch64Linker.class, onlyWith = ForeignAPIPredicates.FunctionCallsSupported.class) final class Target_jdk_internal_foreign_abi_aarch64_linux_LinuxAArch64Linker { + @SuppressWarnings({"static-method", "try", "unused"}) + @Substitute + MethodHandle arrangeDowncall(MethodType inferredMethodType, FunctionDescriptor function, LinkerOptions options) { + try (LinkRequest ignore = LinkRequest.create(false, function, options)) { + return jdk.internal.foreign.abi.aarch64.CallArranger.LINUX.arrangeDowncall(inferredMethodType, function, options); + } + } + @SuppressWarnings("static-method") @Substitute UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor function, LinkerOptions options) {