Skip to content

Commit 9d60bc5

Browse files
authored
Support using FFM in native-image (#269)
1 parent 1e4edb5 commit 9d60bc5

12 files changed

Lines changed: 350 additions & 124 deletions

pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@
109109
</properties>
110110

111111
<dependencies>
112+
<dependency>
113+
<groupId>org.graalvm.sdk</groupId>
114+
<artifactId>nativeimage</artifactId>
115+
<version>23.1.0</version>
116+
<scope>provided</scope>
117+
<optional>true</optional>
118+
</dependency>
112119
<dependency>
113120
<groupId>org.junit.jupiter</groupId>
114121
<artifactId>junit-jupiter</artifactId>
@@ -260,6 +267,7 @@
260267
<configuration>
261268
<jvmVersion>9</jvmVersion>
262269
<module>
270+
<mainClass>org.fusesource.jansi.AnsiMain</mainClass>
263271
<moduleInfo>
264272
<name>org.fusesource.jansi</name>
265273
<exports>org.fusesource.jansi;

src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupport.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020

2121
import org.fusesource.jansi.io.AnsiProcessor;
2222

23-
public interface AnsiConsoleSupport {
23+
public abstract class AnsiConsoleSupport {
2424

25-
interface CLibrary {
25+
public interface CLibrary {
2626

2727
int STDOUT_FILENO = 1;
2828
int STDERR_FILENO = 2;
@@ -32,7 +32,7 @@ interface CLibrary {
3232
int isTty(int fd);
3333
}
3434

35-
interface Kernel32 {
35+
public interface Kernel32 {
3636

3737
int isTty(long console);
3838

@@ -51,9 +51,39 @@ interface Kernel32 {
5151
AnsiProcessor newProcessor(OutputStream os, long console) throws IOException;
5252
}
5353

54-
String getProviderName();
54+
private final String providerName;
55+
private CLibrary cLibrary;
56+
private Kernel32 kernel32;
5557

56-
CLibrary getCLibrary();
58+
protected AnsiConsoleSupport(String providerName) {
59+
this.providerName = providerName;
60+
}
61+
62+
public final String getProviderName() {
63+
return providerName;
64+
}
65+
66+
protected abstract CLibrary createCLibrary();
67+
68+
protected abstract Kernel32 createKernel32();
69+
70+
public final CLibrary getCLibrary() {
71+
if (cLibrary == null) {
72+
cLibrary = createCLibrary();
73+
}
5774

58-
Kernel32 getKernel32();
75+
return cLibrary;
76+
}
77+
78+
public final Kernel32 getKernel32() {
79+
if (kernel32 == null) {
80+
if (!OSInfo.isWindows()) {
81+
throw new RuntimeException("Kernel32 is not available on this platform");
82+
}
83+
84+
kernel32 = createKernel32();
85+
}
86+
87+
return kernel32;
88+
}
5989
}

src/main/java/org/fusesource/jansi/internal/AnsiConsoleSupportHolder.java

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,20 @@
1919

2020
public final class AnsiConsoleSupportHolder {
2121

22-
private static final String PROVIDER_NAME;
23-
private static final AnsiConsoleSupport.CLibrary CLIBRARY;
24-
private static final AnsiConsoleSupport.Kernel32 KERNEL32;
25-
private static final Throwable ERR;
22+
static final AnsiConsoleSupport PROVIDER;
23+
24+
static final Throwable ERR;
2625

2726
private static AnsiConsoleSupport getDefaultProvider() {
28-
try {
29-
// Call the specialized constructor to check whether the module has native access enabled
30-
// If not, fallback to JNI to avoid the JDK printing warnings in stderr
31-
return (AnsiConsoleSupport) Class.forName("org.fusesource.jansi.internal.ffm.AnsiConsoleSupportImpl")
32-
.getConstructor(boolean.class)
33-
.newInstance(true);
34-
} catch (Throwable ignored) {
27+
if (!OSInfo.isInImageCode()) {
28+
try {
29+
// Call the specialized constructor to check whether the module has native access enabled
30+
// If not, fallback to JNI to avoid the JDK printing warnings in stderr
31+
return (AnsiConsoleSupport) Class.forName("org.fusesource.jansi.internal.ffm.AnsiConsoleSupportImpl")
32+
.getConstructor(boolean.class)
33+
.newInstance(true);
34+
} catch (Throwable ignored) {
35+
}
3536
}
3637

3738
return new org.fusesource.jansi.internal.jni.AnsiConsoleSupportImpl();
@@ -81,47 +82,26 @@ private static AnsiConsoleSupport findProvider(String providerList) {
8182
err = e;
8283
}
8384

84-
String providerName = null;
85-
AnsiConsoleSupport.CLibrary clib = null;
86-
AnsiConsoleSupport.Kernel32 kernel32 = null;
85+
PROVIDER = ansiConsoleSupport;
86+
ERR = err;
87+
}
8788

88-
if (ansiConsoleSupport != null) {
89-
try {
90-
providerName = ansiConsoleSupport.getProviderName();
91-
clib = ansiConsoleSupport.getCLibrary();
92-
kernel32 = OSInfo.isWindows() ? ansiConsoleSupport.getKernel32() : null;
93-
} catch (Throwable e) {
94-
err = e;
95-
}
89+
public static AnsiConsoleSupport getProvider() {
90+
if (PROVIDER == null) {
91+
throw new RuntimeException("No provider available", ERR);
9692
}
97-
98-
PROVIDER_NAME = providerName;
99-
CLIBRARY = clib;
100-
KERNEL32 = kernel32;
101-
ERR = err;
93+
return PROVIDER;
10294
}
10395

10496
public static String getProviderName() {
105-
return PROVIDER_NAME;
97+
return getProvider().getProviderName();
10698
}
10799

108100
public static AnsiConsoleSupport.CLibrary getCLibrary() {
109-
if (CLIBRARY == null) {
110-
throw new RuntimeException("Unable to get the instance of CLibrary", ERR);
111-
}
112-
113-
return CLIBRARY;
101+
return getProvider().getCLibrary();
114102
}
115103

116104
public static AnsiConsoleSupport.Kernel32 getKernel32() {
117-
if (KERNEL32 == null) {
118-
if (OSInfo.isWindows()) {
119-
throw new RuntimeException("Unable to get the instance of Kernel32", ERR);
120-
} else {
121-
throw new UnsupportedOperationException("Not Windows");
122-
}
123-
}
124-
125-
return KERNEL32;
105+
return getProvider().getKernel32();
126106
}
127107
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (C) 2009-2023 the original author(s).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.fusesource.jansi.internal;
17+
18+
import java.util.Objects;
19+
20+
import org.fusesource.jansi.AnsiConsole;
21+
import org.graalvm.nativeimage.hosted.Feature;
22+
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
23+
import org.graalvm.nativeimage.hosted.RuntimeSystemProperties;
24+
25+
public class NativeImageFeature implements Feature {
26+
@Override
27+
public String getURL() {
28+
return "https://github.com/fusesource/jansi";
29+
}
30+
31+
@Override
32+
public void duringSetup(DuringSetupAccess access) {
33+
RuntimeClassInitialization.initializeAtBuildTime(AnsiConsoleSupportHolder.class);
34+
35+
String providers = System.getProperty(AnsiConsole.JANSI_PROVIDERS);
36+
if (providers != null) {
37+
try {
38+
RuntimeSystemProperties.register(AnsiConsole.JANSI_PROVIDERS, providers);
39+
} catch (Throwable ignored) {
40+
// GraalVM version < 23.0
41+
// No need to worry as we select the provider at build time
42+
}
43+
}
44+
45+
String provider = Objects.requireNonNull(AnsiConsoleSupportHolder.getProviderName(), "No provider available");
46+
if (provider.equals(AnsiConsole.JANSI_PROVIDER_JNI)) {
47+
String jansiNativeLibraryName = System.mapLibraryName("jansi");
48+
if (jansiNativeLibraryName.endsWith(".dylib")) {
49+
jansiNativeLibraryName = jansiNativeLibraryName.replace(".dylib", ".jnilib");
50+
}
51+
52+
String packagePath = JansiLoader.class.getPackage().getName().replace('.', '/');
53+
54+
try {
55+
Class<?> moduleClass = Class.forName("java.lang.Module");
56+
Class<?> rraClass = Class.forName("org.graalvm.nativeimage.hosted.RuntimeResourceAccess");
57+
58+
Object module = Class.class.getMethod("getModule").invoke(JansiLoader.class);
59+
rraClass.getMethod("addResource", moduleClass, String.class)
60+
.invoke(
61+
null,
62+
module,
63+
String.format(
64+
"%s/native/%s/%s",
65+
packagePath,
66+
OSInfo.getNativeLibFolderPathForCurrentOS(),
67+
jansiNativeLibraryName));
68+
69+
} catch (Throwable ignored) {
70+
// GraalVM version < 22.3
71+
// Users need to manually add the JNI library as resources
72+
}
73+
} else if (provider.equals(AnsiConsole.JANSI_PROVIDER_FFM)) {
74+
try {
75+
// FFM is only available in JDK 21+, so we need to compile it separately
76+
Class.forName("org.fusesource.jansi.internal.ffm.NativeImageDowncallRegister")
77+
.getMethod("registerForDowncall")
78+
.invoke(null);
79+
} catch (Throwable e) {
80+
throw new RuntimeException(e);
81+
}
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)