Skip to content

Commit e546906

Browse files
committed
[GR-61511] Add JLine FFM based terminal provider.
PullRequest: graal/19851
2 parents 7eb97fa + 6aad090 commit e546906

File tree

7 files changed

+364
-10
lines changed

7 files changed

+364
-10
lines changed

sdk/mx.sdk/mx_sdk_vm_impl.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ def svm_experimental_options(experimental_options):
124124
'--features=org.graalvm.launcher.PolyglotLauncherFeature',
125125
'--tool:all',
126126
] + svm_experimental_options(['-H:-ParseRuntimeOptions']),
127+
# No need to pass --enable-native-access=org.graalvm.shadowed.jline, because the polyglot bash launcher
128+
# script adds JLine to class-path, not module-path, so we do not need to enable native access
127129
is_main_launcher=True,
128130
default_symlinks=True,
129131
is_sdk_launcher=True,

sdk/mx.sdk/suite.py

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
# SOFTWARE.
4040
#
4141
suite = {
42-
"mxversion": "7.54.3",
42+
"mxversion": "7.54.5",
4343
"name" : "sdk",
4444
"version" : "25.0.0",
4545
"release" : False,
@@ -135,7 +135,6 @@
135135
"version": "3.28.0",
136136
},
137137
},
138-
139138
"JLINE_TERMINAL": {
140139
"moduleName": "org.jline.terminal",
141140
"digest": "sha512:abe0ad0303e5eb81b549301dfdcf34aace14495240816f14302d193296c7a8be31488e468d18a215976b8e4e8fa29f72d830e492eed7d4a6f9f04c81a6e36c3c",
@@ -146,7 +145,6 @@
146145
"version": "3.28.0",
147146
},
148147
},
149-
150148
"JLINE_BUILTINS": {
151149
"moduleName": "org.jline.builtins",
152150
"digest": "sha512:189d893405170a3edc624a6b822a8a394a2f8b623c23aed9e015d4b018b232307408b6038322719155fc7da7e9c04a9bb0a76c8521f49dd86a5f84ea3880acb6",
@@ -157,6 +155,16 @@
157155
"version": "3.28.0",
158156
},
159157
},
158+
"JLINE_TERMINAL_FFM": {
159+
"moduleName": "org.jline.terminal.ffm",
160+
"digest": "sha512:e5839b04a2fd6119a11c6bc16e05203af88512039d85551b19d6e87c358a325ed5eb7051022a225e2641357c99d9c4121817a4795c50cf79a13b6b9d537cee96",
161+
"sourceDigest": "sha512:c651ae99fe1f453d9b3d22913e2fb003c11ff9c43621bedd7508fa322b49f15c3d93cf146c00f2e1f9dd939f3ca9009a52ee407b63fa3f3d4f5c997a1efba139",
162+
"maven": {
163+
"groupId": "org.jline",
164+
"artifactId": "jline-terminal-ffm",
165+
"version": "3.28.0",
166+
},
167+
},
160168
"LLVM_ORG" : {
161169
"version" : "20.1.4-1-ga7183f5a17-bg217527b869",
162170
"host" : "https://lafo.ssw.uni-linz.ac.at/pub/llvm-org",
@@ -684,14 +692,17 @@
684692
"graalCompilerSourceEdition": "ignore",
685693
},
686694
"org.graalvm.shadowed.org.jline": {
687-
# shaded JLINE_*
695+
# shaded custom JLine bundle
688696
"subDir": "src",
689697
"sourceDirs": ["src"],
690698
"javaCompliance": "17+",
691699
"spotbugs": "false",
692700
"requires": [
693701
"java.logging",
694702
],
703+
"dependencies": [
704+
"sdk:NATIVEIMAGE",
705+
],
695706
"shadedDependencies": [
696707
"sdk:JLINE_READER",
697708
"sdk:JLINE_TERMINAL",
@@ -732,6 +743,11 @@ class UniversalDetector {
732743
String getDetectedCharset() { return null; }
733744
}""",
734745
},
746+
# Adds calls to initialize logging. This is a convenient way to enable JLine logging
747+
# in order to verify which terminal provider is used at runtime.
748+
"org/jline/terminal/TerminalBuilder.java": {
749+
"private TerminalBuilder\\(\\) {}": "private TerminalBuilder() { org.graalvm.shadowed.org.jline.terminal.JLineLoggingSupport.init(); }"
750+
},
735751
# Remove dependency on JLine's native library (would require shading and deployment of the library)
736752
# The native library is a fallback for functionality that is otherwise done via accessing
737753
# JDK internals via reflection.
@@ -755,16 +771,24 @@ class UniversalDetector {
755771
import org.graalvm.shadowed.org.jline.terminal.Terminal;
756772
import org.graalvm.shadowed.org.jline.terminal.impl.exec.ExecTerminalProvider;
757773
""",
774+
# \\x7b is to avoid the opening curly brace, which confuses the suite.py parser.
775+
# The commented out closing curly brace at the end is to match the opening
776+
# brace, again, to make the suite.py parser happy.
758777
"static TerminalProvider load\\(String name\\) throws IOException \\x7b":
759778
"""
760779
static TerminalProvider load(String name) throws IOException {
761780
switch (name) {
762781
case \"exec\":
763782
return new ExecTerminalProvider();
783+
case \"ffm\":
784+
TerminalProvider p = org.graalvm.shadowed.org.jline.terminal.impl.ffm.FFMTerminalProviderLoader.load();
785+
if (p != null) {
786+
return p;
787+
}
764788
default:
765-
if (Boolean.TRUE) { // to avoid unreachable code
766-
throw new IOException(\"Unable to find terminal provider \" + name);
767-
}
789+
if (Boolean.TRUE) { // to avoid unreachable code below the switch
790+
throw new IOException(\"Unable to find terminal provider \" + name);
791+
}
768792
}
769793
// }
770794
""",
@@ -778,6 +802,48 @@ class UniversalDetector {
778802
"jacoco": "exclude",
779803
"graalCompilerSourceEdition": "ignore",
780804
},
805+
"org.graalvm.shadowed.org.jline.jdk22": {
806+
# Shaded JLINE_TERMINAL_FFM as jdk22 overlay for the shaded JLine bundle
807+
# Needs --enable-native-access=org.graalvm.shadowed.jline
808+
"subDir": "src",
809+
"sourceDirs": ["src"],
810+
"javaCompliance": "22+",
811+
"spotbugs": "false",
812+
"requires": [
813+
"java.logging",
814+
],
815+
"dependencies": [
816+
"org.graalvm.shadowed.org.jline",
817+
"sdk:NATIVEIMAGE",
818+
],
819+
"shadedDependencies": [
820+
"sdk:JLINE_TERMINAL_FFM",
821+
],
822+
"class": "ShadedLibraryProject",
823+
"shade": {
824+
"packages": {
825+
"org.jline": "org.graalvm.shadowed.org.jline",
826+
},
827+
"exclude": [
828+
"META-INF/MANIFEST.MF",
829+
# we patch the JLine's service loading mechanism with
830+
# hard-coded set of supported services, see one of the patches below
831+
"META-INF/services/**",
832+
"META-INF/maven/**",
833+
# We have our own native-image configuration (in the overlaid project)
834+
"META-INF/native-image/**",
835+
],
836+
},
837+
"description": "JLINE FFM based Terminal service provider.",
838+
"overlayTarget" : "org.graalvm.shadowed.org.jline",
839+
"multiReleaseJarVersion" : "22",
840+
"ignoreSrcGenForOverlayMap": "true",
841+
"allowsJavadocWarnings": True,
842+
"noMavenJavadoc": True,
843+
"javac.lint.overrides": 'none',
844+
"jacoco": "exclude",
845+
"graalCompilerSourceEdition": "ignore",
846+
},
781847
"org.graalvm.maven.downloader" : {
782848
"subDir" : "src",
783849
"sourceDirs" : ["src"],
@@ -1135,7 +1201,12 @@ class UniversalDetector {
11351201
"graalCompilerSourceEdition": "ignore",
11361202
},
11371203
"JLINE3": {
1138-
# shaded JLINE_*
1204+
# Custom shaded JLine bundle (with FFM terminal provider on JDK22+)
1205+
# One must pass --enable-native-access=org.graalvm.shadowed.jline, otherwise
1206+
# JLine silently falls back to exec provider on POSIX, and with a warning
1207+
# to "Dumb" provider on Windows
1208+
# If desired, the FFM terminal on JDK22+ can be disabled at built time using system property:
1209+
# org.graalvm.shadowed.org.jline.terminal.ffm.disable=true
11391210
"moduleInfo": {
11401211
"name": "org.graalvm.shadowed.jline",
11411212
"requires": [
@@ -1161,6 +1232,10 @@ class UniversalDetector {
11611232
"dependencies": [
11621233
"org.graalvm.shadowed.org.jline",
11631234
],
1235+
"distDependencies": [
1236+
"sdk:NATIVEIMAGE",
1237+
"sdk:POLYGLOT",
1238+
],
11641239
"description": "JLINE3 shaded module.",
11651240
"allowsJavadocWarnings": True,
11661241
"license": "BSD-new",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# errors
2+
org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
3+
org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
4+
# warnings
5+
org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
6+
org.eclipse.jdt.core.compiler.problem.deprecation=ignore
7+
org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
8+
org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
9+
org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
10+
org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
11+
org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
12+
org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
13+
org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
14+
org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
15+
org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=ignore
16+
org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
17+
org.eclipse.jdt.core.compiler.problem.unusedLocal=ignore
18+
org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore
19+
org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
20+
org.eclipse.jdt.core.compiler.problem.unusedTypeArgumentsForMethodInvocation=ignore
21+
org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package org.graalvm.shadowed.org.jline.terminal.impl.ffm;
42+
43+
import java.lang.foreign.FunctionDescriptor;
44+
import java.lang.foreign.Linker;
45+
import java.lang.foreign.MemoryLayout;
46+
import java.lang.foreign.StructLayout;
47+
import java.lang.foreign.ValueLayout;
48+
49+
import org.graalvm.nativeimage.Platform;
50+
import org.graalvm.nativeimage.hosted.Feature;
51+
import org.graalvm.nativeimage.hosted.RuntimeForeignAccess;
52+
import org.graalvm.shadowed.org.jline.terminal.spi.TerminalProvider;
53+
54+
public class FFMTerminalProviderLoader {
55+
/**
56+
* Build time flag to disable the FFM provider and avoid pulling FFM related code into the
57+
* image.
58+
*/
59+
static final boolean DISABLED = Boolean.getBoolean("org.graalvm.shadowed.org.jline.terminal.ffm.disable");
60+
61+
public static TerminalProvider load() {
62+
if (DISABLED) {
63+
return null;
64+
}
65+
return new org.graalvm.shadowed.org.jline.terminal.impl.ffm.FfmTerminalProvider();
66+
}
67+
}
68+
69+
class FFMTerminalProviderFeature implements Feature {
70+
private record DowncallDesc(FunctionDescriptor fd, Linker.Option... options) {
71+
}
72+
73+
private static StructLayout WIN_COORD_LAYOUT = MemoryLayout.structLayout(ValueLayout.JAVA_SHORT.withName("x"), ValueLayout.JAVA_SHORT.withName("y"));
74+
75+
// Downcalls from CLibrary.java
76+
private static DowncallDesc[] getUnixDowncalls() {
77+
return new DowncallDesc[]{
78+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.ADDRESS), Linker.Option.firstVariadicArg(2)), // ioctl
79+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)), // isatty
80+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS)), // tcsetattr
81+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS)), // tcgetattr
82+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)), // ttyname_r
83+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)) // openpty
84+
};
85+
}
86+
87+
// Downcalls from Kernel32
88+
private static DowncallDesc[] getWindowsDowncalls() {
89+
return new DowncallDesc[]{
90+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)), // WaitForSingleObject
91+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT)), // GetStdHandle
92+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS,
93+
ValueLayout.JAVA_INT, ValueLayout.ADDRESS)), // FormatMessageW
94+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_SHORT)), // SetConsoleTextAttribute
95+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)), // SetConsoleMode
96+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS)), // GetConsoleMode
97+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)), // SetConsoleTitleW
98+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, WIN_COORD_LAYOUT)), // SetConsoleCursorPosition
99+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_CHAR, ValueLayout.JAVA_INT, WIN_COORD_LAYOUT, ValueLayout.ADDRESS)), // FillConsoleOutputCharacterW
100+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_SHORT, ValueLayout.JAVA_INT, WIN_COORD_LAYOUT, ValueLayout.ADDRESS)), // FillConsoleOutputAttribute
101+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS)), // WriteConsoleW
102+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS)), // ReadConsoleInputW
103+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.ADDRESS)), // PeekConsoleInputW
104+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS)), // GetConsoleScreenBufferInfo
105+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, WIN_COORD_LAYOUT, ValueLayout.ADDRESS)), // ScrollConsoleScreenBufferW
106+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT)), // GetLastError
107+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)), // GetFileType
108+
new DowncallDesc(FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT)), // _get_osfhandle
109+
};
110+
}
111+
112+
private static DowncallDesc[] getDowncalls() {
113+
if (Platform.includedIn(Platform.WINDOWS.class)) {
114+
return getWindowsDowncalls();
115+
}
116+
return getUnixDowncalls();
117+
}
118+
119+
public void duringSetup(DuringSetupAccess access) {
120+
if (FFMTerminalProviderLoader.DISABLED) {
121+
return;
122+
}
123+
for (DowncallDesc downcall : getDowncalls()) {
124+
RuntimeForeignAccess.registerForDowncall(downcall.fd(), (Object[]) downcall.options());
125+
}
126+
}
127+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# AttributedCharSequence reads a system property (for runtime configuration) in static ctor
22
# OSUtils detects current OS capabilities in static ctor
3-
Args = --initialize-at-build-time=org.graalvm.shadowed.org.jline \
4-
--initialize-at-run-time=org.graalvm.shadowed.org.jline.utils.AttributedCharSequence,org.graalvm.shadowed.org.jline.utils.OSUtils \
3+
# FFMTerminalProviderFeature is noop on JDK<=21, on JDK22+ it is overridden using mx overlay mechanism,
4+
# and it registers FFM downcalls for FFM terminal provider
5+
Args = --initialize-at-build-time=org.graalvm.shadowed.org.jline,org.graalvm.shadowed.org.jline.terminal.impl.ffm.FFMTerminalProviderLoader \
6+
--features=org.graalvm.shadowed.org.jline.terminal.impl.ffm.FFMTerminalProviderFeature \
7+
--initialize-at-run-time=org.graalvm.shadowed.org.jline.utils.AttributedCharSequence,org.graalvm.shadowed.org.jline.utils.OSUtils,org.graalvm.shadowed.org.jline.terminal.impl.ffm \

0 commit comments

Comments
 (0)