Skip to content

Commit 42a4082

Browse files
committed
[GR-52576] Introduce specialized upcalls for direct method handles.
PullRequest: graal/19448
2 parents 643e28d + 6f374ca commit 42a4082

File tree

14 files changed

+648
-60
lines changed

14 files changed

+648
-60
lines changed

docs/reference-manual/native-image/ForeignInterface.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@ These two kinds of calls are referred to as "downcalls" and "upcalls" respective
3030
### Looking Up Native Functions
3131

3232
The FFM API provides the `SymbolLookup` interface to find functions in native libraries by name.
33-
`SymbolLookup.loaderLookup()` is currently the only supported kind of `SymbolLookup`.
33+
Native image supports all available symbol lookup methods, i.e., `SymbolLookup.loaderLookup()`, `SymbolLookup.libraryLookup()`, and `Linker.defaultLookup()`.
3434

3535
### Registering Foreign Calls
3636

3737
In order to perform calls to native code at run time, supporting code must be generated at image build time.
38-
Therefore, the `native-image` tool must be provided with descriptors that characterize the functions to which downcalls may be performed at run time.
38+
Therefore, the `native-image` tool must be provided with descriptors that characterize the functions with which downcalls or upcalls can be performed at runtime.
3939

40-
These descriptors can be registered using a custom `Feature`, for example:
40+
For upcalls, it is recommended to register a specific static method as an upcall target by providing its declaring class and the method name.
41+
This allows `native-image` to create specialized upcall code that can be orders of magnitude faster than a upcall registered only by function descriptor.
42+
Whenever possible, this should be the preferred way to register upcalls.
43+
44+
Descriptors and target methods can be registered using a custom `Feature`, for example:
4145
```java
4246
import static java.lang.foreign.ValueLayout.*;
4347

@@ -50,6 +54,9 @@ class ForeignRegistrationFeature implements Feature {
5054
RuntimeForeignAccess.registerForUpcall(FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT));
5155
RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(ADDRESS, JAVA_INT, JAVA_INT), Linker.Option.firstVariadicArg(1));
5256
RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid(JAVA_INT), Linker.Option.captureCallState("errno"));
57+
58+
MethodHandle target = MethodHandles.lookup().findStatic(UserClass.class, "aStaticMethod", MethodType.of(int.class, int.class, int.class));
59+
RuntimeForeignAccess.registerForUpcall(target, FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT));
5360
}
5461
}
5562
```

sdk/src/org.graalvm.nativeimage/snapshot.sigtest

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,7 @@ meth public !varargs static void initializeAtRunTime(java.lang.String[])
10881088
supr java.lang.Object
10891089

10901090
CLSS public final org.graalvm.nativeimage.hosted.RuntimeForeignAccess
1091+
meth public !varargs static void registerForDirectUpcall(java.lang.invoke.MethodHandle,java.lang.Object,java.lang.Object[])
10911092
meth public !varargs static void registerForDowncall(java.lang.Object,java.lang.Object[])
10921093
meth public !varargs static void registerForUpcall(java.lang.Object,java.lang.Object[])
10931094
supr java.lang.Object

sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/RuntimeForeignAccess.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
*/
4141
package org.graalvm.nativeimage.hosted;
4242

43+
import java.lang.invoke.MethodHandle;
44+
import java.lang.invoke.MethodType;
45+
4346
import org.graalvm.nativeimage.ImageSingletons;
4447
import org.graalvm.nativeimage.Platform;
4548
import org.graalvm.nativeimage.Platforms;
@@ -86,6 +89,34 @@ public static void registerForUpcall(Object desc, Object... options) {
8689
ImageSingletons.lookup(RuntimeForeignAccessSupport.class).registerForUpcall(ConfigurationCondition.alwaysTrue(), desc, options);
8790
}
8891

92+
/**
93+
* Registers a specific static method (denoted by a method handle) as a fast upcall target. This
94+
* will create a specialized upcall stub that will invoke only the specified method, which is
95+
* much faster than using {@link #registerForUpcall(Object, Object...)}).
96+
* <p>
97+
* The provided method handle must be a direct method handle. Those are most commonly created
98+
* using {@link java.lang.invoke.MethodHandles.Lookup#findStatic(Class, String, MethodType)}.
99+
* However, a strict requirement is that it must be possible to create a non-empty descriptor
100+
* for the method handle using {@link MethodHandle#describeConstable()}. The denoted static
101+
* method will also be registered for reflective access since run-time code will also create a
102+
* method handle to denoted static method.
103+
* </p>
104+
* <p>
105+
* Even though this method is weakly typed for compatibility reasons, runtime checks will be
106+
* performed to ensure that the arguments have the expected type. It will be deprecated in favor
107+
* of strongly typed variant as soon as possible.
108+
* </p>
109+
*
110+
* @param target A direct method handle denoting a static method.
111+
* @param desc A {@link java.lang.foreign.FunctionDescriptor} to register for upcalls.
112+
* @param options An array of {@link java.lang.foreign.Linker.Option} used for the upcalls.
113+
*
114+
* @since 24.2
115+
*/
116+
public static void registerForDirectUpcall(MethodHandle target, Object desc, Object... options) {
117+
ImageSingletons.lookup(RuntimeForeignAccessSupport.class).registerForDirectUpcall(ConfigurationCondition.alwaysTrue(), target, desc, options);
118+
}
119+
89120
private RuntimeForeignAccess() {
90121
}
91122
}

sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/RuntimeForeignAccessSupport.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -40,8 +40,12 @@
4040
*/
4141
package org.graalvm.nativeimage.impl;
4242

43+
import java.lang.invoke.MethodHandle;
44+
4345
public interface RuntimeForeignAccessSupport {
4446
void registerForDowncall(ConfigurationCondition condition, Object desc, Object... options);
4547

4648
void registerForUpcall(ConfigurationCondition condition, Object desc, Object... options);
49+
50+
void registerForDirectUpcall(ConfigurationCondition condition, MethodHandle target, Object desc, Object... options);
4751
}

substratevm/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ At runtime, premain runtime options are set along with main class' arguments in
1010
The warning is planned to be replaced by an error in GraalVM for JDK 25.
1111
* (GR-48384) Added a GDB Python script (`gdb-debughelpers.py`) to improve the Native Image debugging experience.
1212
* (GR-49517) Add support for emitting Windows x64 unwind info. This enables stack walking in native tooling such as debuggers and profilers.
13+
* (GR-52576) Optimize FFM API upcalls for specifiable static upcall target methods.
1314
* (GR-56599) Update native image debuginfo from DWARF4 to DWARF5 and store type information for debugging in DWARF type units.
1415
* (GR-56601) Together with Red Hat, we added experimental support for `jcmd` on Linux and macOS. Add `--enable-monitoring=jcmd` to your build arguments to try it out.
1516
* (GR-57384) Preserve the origin of a resource included in a native image. The information is included in the report produced by -H:+GenerateEmbeddedResourcesFile.

substratevm/mx.substratevm/suite.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,13 +719,13 @@
719719
],
720720
"requiresConcealed": {
721721
"java.base": [
722-
"jdk.internal.loader",
723-
"jdk.internal.reflect",
724722
"jdk.internal.foreign",
725723
"jdk.internal.foreign.abi",
726724
"jdk.internal.foreign.abi.x64",
727725
"jdk.internal.foreign.abi.x64.sysv",
728726
"jdk.internal.foreign.abi.x64.windows",
727+
"jdk.internal.loader",
728+
"jdk.internal.reflect",
729729
],
730730
"jdk.internal.vm.ci" : [
731731
"jdk.vm.ci.amd64",
@@ -758,6 +758,9 @@
758758
"java.base": [
759759
"jdk.internal.foreign",
760760
"jdk.internal.foreign.abi",
761+
"jdk.internal.foreign.abi.x64.windows",
762+
"jdk.internal.foreign.abi.x64.sysv",
763+
"jdk.internal.foreign.layout",
761764
],
762765
"jdk.internal.vm.ci" : [
763766
"jdk.vm.ci.code",

substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/ForeignFunctionsRuntime.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626

2727
import static jdk.graal.compiler.core.common.spi.ForeignCallDescriptor.CallSideEffect.HAS_SIDE_EFFECT;
2828

29+
import java.lang.constant.DirectMethodHandleDesc;
2930
import java.lang.invoke.MethodHandle;
3031
import java.util.HashMap;
3132
import java.util.Map;
33+
import java.util.function.BiConsumer;
3234

3335
import org.graalvm.collections.EconomicMap;
3436
import org.graalvm.nativeimage.ImageSingletons;
@@ -61,11 +63,15 @@ public static ForeignFunctionsRuntime singleton() {
6163

6264
private final AbiUtils.TrampolineTemplate trampolineTemplate = AbiUtils.singleton().generateTrampolineTemplate();
6365
private final EconomicMap<NativeEntryPointInfo, FunctionPointerHolder> downcallStubs = EconomicMap.create();
66+
private final EconomicMap<DirectMethodHandleDesc, FunctionPointerHolder> directUpcallStubs = EconomicMap.create();
6467
private final EconomicMap<JavaEntryPointInfo, FunctionPointerHolder> upcallStubs = EconomicMap.create();
6568

6669
private final Map<Long, TrampolineSet> trampolines = new HashMap<>();
6770
private TrampolineSet currentTrampolineSet;
6871

72+
// for testing: callback if direct upcall lookup succeeded
73+
private BiConsumer<Long, DirectMethodHandleDesc> usingSpecializedUpcallListener;
74+
6975
@Platforms(Platform.HOSTED_ONLY.class)
7076
public ForeignFunctionsRuntime() {
7177
}
@@ -82,6 +88,12 @@ public void addUpcallStubPointer(JavaEntryPointInfo jep, CFunctionPointer ptr) {
8288
VMError.guarantee(upcallStubs.put(jep, new FunctionPointerHolder(ptr)) == null);
8389
}
8490

91+
@Platforms(Platform.HOSTED_ONLY.class)
92+
public void addDirectUpcallStubPointer(DirectMethodHandleDesc desc, CFunctionPointer ptr) {
93+
VMError.guarantee(!directUpcallStubs.containsKey(desc), "Seems like multiple stubs were generated for " + desc);
94+
VMError.guarantee(directUpcallStubs.put(desc, new FunctionPointerHolder(ptr)) == null);
95+
}
96+
8597
/**
8698
* We'd rather report the function descriptor than the native method type, but we don't have it
8799
* available here. One could intercept this exception in
@@ -105,15 +117,61 @@ CFunctionPointer getUpcallStubPointer(JavaEntryPointInfo jep) {
105117
}
106118

107119
Pointer registerForUpcall(MethodHandle methodHandle, JavaEntryPointInfo jep) {
120+
/*
121+
* Look up the upcall stub pointer first to avoid unnecessary allocation and synchronization
122+
* if it doesn't exist.
123+
*/
124+
CFunctionPointer upcallStubPointer = getUpcallStubPointer(jep);
108125
synchronized (trampolines) {
109126
if (currentTrampolineSet == null || !currentTrampolineSet.hasFreeTrampolines()) {
110127
currentTrampolineSet = new TrampolineSet(trampolineTemplate);
111128
trampolines.put(currentTrampolineSet.base().rawValue(), currentTrampolineSet);
112129
}
113-
return currentTrampolineSet.assignTrampoline(methodHandle, getUpcallStubPointer(jep));
130+
return currentTrampolineSet.assignTrampoline(methodHandle, upcallStubPointer);
114131
}
115132
}
116133

134+
/**
135+
* Updates the stub address in the upcall trampoline with the address of a direct upcall stub.
136+
* The trampoline is identified by the given native address and the direct upcall stub is
137+
* identified by the method handle descriptor.
138+
*
139+
* @param trampolineAddress The address of the upcall trampoline.
140+
* @param desc A direct method handle descriptor used to lookup the direct upcall stub.
141+
*/
142+
void patchForDirectUpcall(long trampolineAddress, DirectMethodHandleDesc desc) {
143+
FunctionPointerHolder functionPointerHolder = directUpcallStubs.get(desc);
144+
if (functionPointerHolder == null) {
145+
return;
146+
}
147+
148+
Pointer trampolinePointer = WordFactory.pointer(trampolineAddress);
149+
Pointer trampolineSetBase = TrampolineSet.getAllocationBase(trampolinePointer);
150+
TrampolineSet trampolineSet = trampolines.get(trampolineSetBase.rawValue());
151+
if (trampolineSet == null) {
152+
return;
153+
}
154+
/*
155+
* Synchronizing on 'trampolineSet' is not necessary at this point since we are still in the
156+
* call context of 'Linker.upcallStub' and the allocated trampoline is owned by the
157+
* allocating thread until it returns from the call. Also, the trampoline cannot be free'd
158+
* between allocation and patching because the associated arena is still on the stack.
159+
*/
160+
trampolineSet.patchTrampolineForDirectUpcall(trampolinePointer, functionPointerHolder.functionPointer);
161+
/*
162+
* If we reach this point, everything went fine and the trampoline was patched with the
163+
* specialized upcall stub's address. For testing, now report that the lookup and patching
164+
* succeeded.
165+
*/
166+
if (usingSpecializedUpcallListener != null) {
167+
usingSpecializedUpcallListener.accept(trampolineAddress, desc);
168+
}
169+
}
170+
171+
public void setUsingSpecializedUpcallListener(BiConsumer<Long, DirectMethodHandleDesc> listener) {
172+
usingSpecializedUpcallListener = listener;
173+
}
174+
117175
void freeTrampoline(long addr) {
118176
synchronized (trampolines) {
119177
long base = TrampolineSet.getAllocationBase(WordFactory.pointer(addr)).rawValue();

substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_AbstractLinker.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,25 @@
2424
*/
2525
package com.oracle.svm.core.foreign;
2626

27+
import java.lang.constant.DirectMethodHandleDesc;
28+
import java.lang.constant.MethodHandleDesc;
29+
import java.lang.foreign.Arena;
30+
import java.lang.foreign.FunctionDescriptor;
31+
import java.lang.foreign.MemorySegment;
32+
import java.lang.invoke.MethodHandle;
33+
import java.lang.invoke.MethodType;
34+
import java.util.Optional;
35+
2736
import com.oracle.svm.core.annotate.Alias;
2837
import com.oracle.svm.core.annotate.RecomputeFieldValue;
38+
import com.oracle.svm.core.annotate.Substitute;
2939
import com.oracle.svm.core.annotate.TargetClass;
3040

3141
import jdk.internal.foreign.abi.AbstractLinker;
42+
import jdk.internal.foreign.abi.AbstractLinker.UpcallStubFactory;
43+
import jdk.internal.foreign.abi.LinkerOptions;
44+
import jdk.internal.foreign.abi.x64.sysv.SysVx64Linker;
45+
import jdk.internal.foreign.abi.x64.windows.Windowsx64Linker;
3246

3347
@TargetClass(AbstractLinker.class)
3448
public final class Target_jdk_internal_foreign_abi_AbstractLinker {
@@ -46,3 +60,55 @@ public final class Target_jdk_internal_foreign_abi_AbstractLinker {
4660
@TargetClass(className = "jdk.internal.foreign.abi.SoftReferenceCache")
4761
final class Target_jdk_internal_foreign_abi_SoftReferenceCache {
4862
}
63+
64+
/**
65+
* A decorator for jdk.internal.foreign.abi.UpcallStubFactory which intercepts the call to method
66+
* 'makeStub'. It will (1) call the original factory to create the upcall, and (2) then use the
67+
* method handle's descriptor to lookup if a specialized (direct) upcall stub is available. If so,
68+
* the trampoline will be updated with the specialized stub's address.
69+
*
70+
* @param delegate The original upcall stub factory as created by JDK's call arranger.
71+
*/
72+
record UpcallStubFactoryDecorator(UpcallStubFactory delegate) implements UpcallStubFactory {
73+
74+
@Override
75+
public MemorySegment makeStub(MethodHandle target, Arena arena) {
76+
MemorySegment segment = delegate.makeStub(target, arena);
77+
78+
/*
79+
* We cannot do this in 'UpcallLinker.makeUpcallStub' because that one already gets a
80+
* different method handle that will handle parameter/return value bindings. Further, method
81+
* handles cannot be compared. If the provided method handle is a DirectMethodHandle, we use
82+
* the MH descriptor to check if there is a registered direct upcall stub. Then, we will
83+
* patch the already allocated trampoline with a different upcall stub pointer.
84+
*/
85+
Optional<MethodHandleDesc> methodHandleDesc = target.describeConstable();
86+
if (methodHandleDesc.isPresent() && methodHandleDesc.get() instanceof DirectMethodHandleDesc desc) {
87+
ForeignFunctionsRuntime.singleton().patchForDirectUpcall(segment.address(), desc);
88+
}
89+
return segment;
90+
}
91+
}
92+
93+
@TargetClass(value = SysVx64Linker.class, onlyWith = ForeignFunctionsEnabled.class)
94+
final class Target_jdk_internal_foreign_abi_x64_sysv_SysVx64Linker {
95+
96+
@Substitute
97+
UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor function, LinkerOptions options) {
98+
return new UpcallStubFactoryDecorator(jdk.internal.foreign.abi.x64.sysv.CallArranger.arrangeUpcall(targetType, function, options));
99+
}
100+
}
101+
102+
@TargetClass(value = Windowsx64Linker.class, onlyWith = ForeignFunctionsEnabled.class)
103+
final class Target_jdk_internal_foreign_abi_x64_windows_Windowsx64Linker {
104+
105+
@Substitute
106+
UpcallStubFactory arrangeUpcall(MethodType targetType, FunctionDescriptor function, LinkerOptions options) {
107+
return new UpcallStubFactoryDecorator(jdk.internal.foreign.abi.x64.windows.CallArranger.arrangeUpcall(targetType, function, options));
108+
}
109+
}
110+
111+
/*
112+
* GR-58659, GR-58660: add substitutions for LinuxAArch64Linker and MacOsAArch64Linker here once we
113+
* support them.
114+
*/

0 commit comments

Comments
 (0)