mirror of
https://github.com/JetBrains/JetBrainsRuntime.git
synced 2026-01-31 04:40:54 +01:00
Compare commits
315 Commits
vpr/update
...
jbr25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e50f535947 | ||
|
|
81b06e0189 | ||
|
|
ad6422a063 | ||
|
|
32de5c51f7 | ||
|
|
3e0117342f | ||
|
|
dd05ed48b0 | ||
|
|
f1c768b55e | ||
|
|
685515eecc | ||
|
|
a00b25a4e8 | ||
|
|
7a2bc8fb5f | ||
|
|
1804a6246c | ||
|
|
ed1504532c | ||
|
|
3a5672a379 | ||
|
|
7feb1a32bb | ||
|
|
f12891f1ac | ||
|
|
ff0e89986c | ||
|
|
001d5f6455 | ||
|
|
af5f3bf1e6 | ||
|
|
f8e1f57602 | ||
|
|
75990bd8e2 | ||
|
|
6e1db512e3 | ||
|
|
03de0be867 | ||
|
|
2fc02dac34 | ||
|
|
9cee39e654 | ||
|
|
3bd3d490eb | ||
|
|
8240aeca9f | ||
|
|
2f5ff016b8 | ||
|
|
17129fe6bb | ||
|
|
8b5e4ca797 | ||
|
|
d27facbd40 | ||
|
|
5dca6876b5 | ||
|
|
79ce7b4661 | ||
|
|
997872a738 | ||
|
|
284d3dfb5d | ||
|
|
cef0f89a96 | ||
|
|
a8f9bc4af5 | ||
|
|
229f73d07a | ||
|
|
a66cd0f996 | ||
|
|
1c89084d0c | ||
|
|
ab06ebc1cc | ||
|
|
af6909947d | ||
|
|
9154864e32 | ||
|
|
5b0b9337d9 | ||
|
|
08333f1417 | ||
|
|
5dde40c38c | ||
|
|
e98d0a40f4 | ||
|
|
41fc238b6e | ||
|
|
72e981d9ff | ||
|
|
5738657709 | ||
|
|
0a85eed507 | ||
|
|
65d9ea8779 | ||
|
|
0e6caec683 | ||
|
|
2b147bd854 | ||
|
|
3f695563ad | ||
|
|
733e9b9ca5 | ||
|
|
838202355a | ||
|
|
90e384351f | ||
|
|
4eff4bd41a | ||
|
|
f4c51b1ccf | ||
|
|
7751aedec7 | ||
|
|
4eb2c10cec | ||
|
|
58c71372c4 | ||
|
|
eb8f788cc7 | ||
|
|
60b0b5ee71 | ||
|
|
e2a4e9c612 | ||
|
|
ff5d111051 | ||
|
|
37ea549e72 | ||
|
|
ac9474f1b4 | ||
|
|
c9c0e5fd46 | ||
|
|
a4a1835477 | ||
|
|
7a73f74456 | ||
|
|
bb7a4ec838 | ||
|
|
458962450c | ||
|
|
17ca88eceb | ||
|
|
6c2dc3f19c | ||
|
|
ba4ef8208b | ||
|
|
4f78a57a32 | ||
|
|
341deb2edf | ||
|
|
b15b77e3ce | ||
|
|
9e13eab915 | ||
|
|
1d794d79e7 | ||
|
|
cec01d5097 | ||
|
|
3b56b96197 | ||
|
|
2b62151aa2 | ||
|
|
56f90a9b19 | ||
|
|
5d7e5bf523 | ||
|
|
da02cac1a9 | ||
|
|
c30857e839 | ||
|
|
4b40e9df0b | ||
|
|
c36c6fedc6 | ||
|
|
8d82199ba0 | ||
|
|
55e37d7ac9 | ||
|
|
802df893a7 | ||
|
|
5ca87846a5 | ||
|
|
7ca9502d22 | ||
|
|
ded81c2c79 | ||
|
|
d60a74254b | ||
|
|
d373a77eae | ||
|
|
811e868e33 | ||
|
|
487aef31ac | ||
|
|
3236aa1abc | ||
|
|
cf9d1bfb3d | ||
|
|
378811aa9c | ||
|
|
7382a8417f | ||
|
|
2e616df413 | ||
|
|
669f1a84a9 | ||
|
|
8cc1a3ff31 | ||
|
|
3c6b7846f6 | ||
|
|
1f49644e38 | ||
|
|
4c533429fc | ||
|
|
ba53556b63 | ||
|
|
a0e47a9c45 | ||
|
|
55baf76d3e | ||
|
|
be32c100e5 | ||
|
|
61bda7303a | ||
|
|
1f7e9e2f32 | ||
|
|
662d55871c | ||
|
|
da49569719 | ||
|
|
a4a960df26 | ||
|
|
34bf02361c | ||
|
|
4f939ef841 | ||
|
|
de815407a0 | ||
|
|
0a72d8c5bb | ||
|
|
75a839af21 | ||
|
|
82c76143c8 | ||
|
|
18d08f6e53 | ||
|
|
f4e344514e | ||
|
|
7352ba6b0f | ||
|
|
d498dc56be | ||
|
|
81e0eb74c0 | ||
|
|
8181062072 | ||
|
|
e54a9e818b | ||
|
|
d02a23474a | ||
|
|
0934306889 | ||
|
|
84fe27bab7 | ||
|
|
be90488ef9 | ||
|
|
d394eea2e9 | ||
|
|
c6fc78c733 | ||
|
|
aea816d37f | ||
|
|
104df6aa39 | ||
|
|
cbc2654510 | ||
|
|
37e4033b08 | ||
|
|
f369141fd6 | ||
|
|
5b5e8660ff | ||
|
|
8973c90a03 | ||
|
|
cff64d2be9 | ||
|
|
e26580de05 | ||
|
|
d01cab4969 | ||
|
|
2fdb7cf0eb | ||
|
|
1d918ce8b1 | ||
|
|
2e50003535 | ||
|
|
051add6c5a | ||
|
|
d498714c1b | ||
|
|
da7714a67d | ||
|
|
8d131d94ee | ||
|
|
415aaf6382 | ||
|
|
80a425d6b0 | ||
|
|
6710bb48cf | ||
|
|
e3a8de312f | ||
|
|
959426d180 | ||
|
|
3528d56c47 | ||
|
|
36d897a328 | ||
|
|
2619ee1c67 | ||
|
|
7949ae4ada | ||
|
|
d4582460f4 | ||
|
|
790a18cc26 | ||
|
|
b1d7577e7f | ||
|
|
d01d8eb151 | ||
|
|
b0926c1895 | ||
|
|
063f0f0ec4 | ||
|
|
d36d9d6188 | ||
|
|
a0573824c1 | ||
|
|
77c10f761f | ||
|
|
9b6e87f41c | ||
|
|
44ccc285b7 | ||
|
|
08153c6ce0 | ||
|
|
0d87088aa6 | ||
|
|
e8f50a83f6 | ||
|
|
ed00e13bab | ||
|
|
48bd6ccbbe | ||
|
|
723039e451 | ||
|
|
54d27a20c7 | ||
|
|
3e7a925c57 | ||
|
|
b333aaf549 | ||
|
|
0361807740 | ||
|
|
8db3d87e02 | ||
|
|
a9f70cbc11 | ||
|
|
675526eeb2 | ||
|
|
8c5a7608d7 | ||
|
|
9c9d863c5e | ||
|
|
f9cd40e7d9 | ||
|
|
3de6f63c05 | ||
|
|
f6844d10d7 | ||
|
|
80d24b5275 | ||
|
|
4f103f1b78 | ||
|
|
29d0bd1fcc | ||
|
|
39ca946862 | ||
|
|
657e0268a2 | ||
|
|
fa9d53ad19 | ||
|
|
0cf915fa95 | ||
|
|
d648ecdf81 | ||
|
|
4bfecd8787 | ||
|
|
33cfc5f9fa | ||
|
|
1657bc2410 | ||
|
|
ea2b0d1b90 | ||
|
|
30d67ee58b | ||
|
|
bc22999ef2 | ||
|
|
1ec08d5dd6 | ||
|
|
f556b3ea5b | ||
|
|
e8aaee9d61 | ||
|
|
91b316fe36 | ||
|
|
c168a53f83 | ||
|
|
f5d63953b7 | ||
|
|
5bc4dded89 | ||
|
|
a4852743ae | ||
|
|
1acafda29c | ||
|
|
30cb64ec03 | ||
|
|
7acd0aac8e | ||
|
|
c2f568c46f | ||
|
|
7af848abaa | ||
|
|
4c08bc4e49 | ||
|
|
09088bc4cf | ||
|
|
782dd27206 | ||
|
|
44317b5925 | ||
|
|
83a20ef619 | ||
|
|
58a158bbfc | ||
|
|
f39e740e0f | ||
|
|
64e09dd3c0 | ||
|
|
b3c8a388c3 | ||
|
|
646c9e95c9 | ||
|
|
42f03c9a6e | ||
|
|
7254380239 | ||
|
|
0783ab090b | ||
|
|
b4638d2f41 | ||
|
|
9b445dba6f | ||
|
|
c3a8b679cb | ||
|
|
f00eb3d0b1 | ||
|
|
beb154f649 | ||
|
|
34e79ffc01 | ||
|
|
6851d806b9 | ||
|
|
bbc94f6986 | ||
|
|
c1cdce3424 | ||
|
|
f5e833e53a | ||
|
|
a8f84b1fd9 | ||
|
|
d4909a4a5c | ||
|
|
ed56cdea5f | ||
|
|
06435e0bd7 | ||
|
|
e6fbe2620a | ||
|
|
72628ee8fb | ||
|
|
3b7f0839f0 | ||
|
|
ae40c215e8 | ||
|
|
0d9a9b59f4 | ||
|
|
98de6bd6d8 | ||
|
|
e6779bfd57 | ||
|
|
df45a19642 | ||
|
|
4c62039d1d | ||
|
|
67646d48fa | ||
|
|
a102d262e0 | ||
|
|
253c5074e7 | ||
|
|
b40240ff2a | ||
|
|
f1aa74d323 | ||
|
|
e43626e130 | ||
|
|
cd50ccf8d3 | ||
|
|
d31ac22aa4 | ||
|
|
7766d8adb3 | ||
|
|
541ef99823 | ||
|
|
bd718557b6 | ||
|
|
17d4b3e5a5 | ||
|
|
9228f16f6c | ||
|
|
640ab99d5a | ||
|
|
12a6bf90e9 | ||
|
|
0b8eb84de1 | ||
|
|
58ba6582cc | ||
|
|
5c7cfac06a | ||
|
|
437432aaa0 | ||
|
|
46de53bb10 | ||
|
|
e372a51bfe | ||
|
|
feb1877e32 | ||
|
|
406fb26778 | ||
|
|
f2e04acb95 | ||
|
|
370c9233bc | ||
|
|
9bd52af67e | ||
|
|
9a617c8826 | ||
|
|
d9b3c73ca5 | ||
|
|
87642d4a7c | ||
|
|
99f37afe26 | ||
|
|
9499121dbf | ||
|
|
5c09f2e93b | ||
|
|
fbe6b30d74 | ||
|
|
6d5042288a | ||
|
|
e768046831 | ||
|
|
462284c399 | ||
|
|
982f64f6aa | ||
|
|
3d4c442497 | ||
|
|
cc70761641 | ||
|
|
2e451d45e7 | ||
|
|
7fb6b4c8a5 | ||
|
|
e245c24ed0 | ||
|
|
db37e6e95b | ||
|
|
492a761f29 | ||
|
|
c82c045321 | ||
|
|
3060684607 | ||
|
|
e1ce2cc75e | ||
|
|
4abbb3d61e | ||
|
|
f63640d867 | ||
|
|
836961f4a5 | ||
|
|
6d8b977b63 | ||
|
|
49946dbc4a | ||
|
|
51098b8024 | ||
|
|
09d78d723c | ||
|
|
3e04f02c07 | ||
|
|
4d2fcd4d35 | ||
|
|
7dca0ae64d | ||
|
|
bf2375a9b8 | ||
|
|
2acd509800 |
@@ -295,7 +295,7 @@ define SetupJavaCompilationBody
|
||||
|
||||
ifeq ($$($1_PROCESS_JBR_API), true)
|
||||
# Automatic path conversion doesn't work for two arguments, so call fixpath manually
|
||||
$1_JBR_API_FLAGS := -Xplugin:"jbr-api $$(call FixPath, $$($1_BIN)/java.base/META-INF/jbrapi.registry) $$(call FixPath, $(TOPDIR)/jb/jbr-api.version)"
|
||||
$1_JBR_API_FLAGS := -Xplugin:"jbr-api $$(call FixPath, $$($1_BIN)/java.base/META-INF/jbrapi) $$(call FixPath, $(TOPDIR)/jb/jbr-api.version)"
|
||||
$1_EXTRA_DEPS := $$($1_EXTRA_DEPS) $$(BUILDTOOLS_OUTPUTDIR)/plugins/_the.COMPILE_JBR_API_PLUGIN_batch
|
||||
endif
|
||||
|
||||
|
||||
@@ -198,34 +198,35 @@ public class JBRApiPlugin implements Plugin {
|
||||
return unresolvedErrors;
|
||||
}
|
||||
|
||||
void read(RandomAccessFile file) throws IOException {
|
||||
void read(RandomAccessFile file, boolean internal) throws IOException {
|
||||
String s;
|
||||
while ((s = file.readLine()) != null) {
|
||||
String[] tokens = s.split(" ");
|
||||
switch (tokens[0]) {
|
||||
case "TYPE" -> {
|
||||
types.put(tokens[1], new Type(tokens[2], Binding.valueOf(tokens[3])));
|
||||
if (tokens.length > 4 && tokens[4].equals("INTERNAL")) internal.add(tokens[1]);
|
||||
}
|
||||
case "VERSION" -> {}
|
||||
case "STATIC" -> {
|
||||
StaticDescriptor descriptor = new StaticDescriptor(new StaticMethod(
|
||||
tokens[1], tokens[2]), tokens[3]);
|
||||
methods.put(descriptor, new StaticMethod(tokens[4], tokens[5]));
|
||||
if (tokens.length > 6 && tokens[6].equals("INTERNAL")) internal.add(descriptor);
|
||||
if (internal) this.internal.add(descriptor);
|
||||
}
|
||||
default -> {
|
||||
types.put(tokens[1], new Type(tokens[2], Binding.valueOf(tokens[0])));
|
||||
if (internal) this.internal.add(tokens[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void write(RandomAccessFile file) throws IOException {
|
||||
void write(RandomAccessFile pub, RandomAccessFile priv) throws IOException {
|
||||
for (var t : types.entrySet()) {
|
||||
file.writeBytes("TYPE " + t.getKey() + " " + t.getValue().type + " " + t.getValue().binding +
|
||||
(internal.contains(t.getKey()) ? " INTERNAL\n" : "\n"));
|
||||
(internal.contains(t.getKey()) ? priv : pub).writeBytes(
|
||||
t.getValue().binding + " " + t.getKey() + " " + t.getValue().type + "\n");
|
||||
}
|
||||
for (var t : methods.entrySet()) {
|
||||
file.writeBytes("STATIC " + t.getKey().method.type + " " + t.getKey().method.name + " " +
|
||||
t.getKey().descriptor + " " + t.getValue().type + " " + t.getValue().name +
|
||||
(internal.contains(t.getKey()) ? " INTERNAL\n" : "\n"));
|
||||
(internal.contains(t.getKey()) ? priv : pub).writeBytes(
|
||||
"STATIC " + t.getKey().method.type + " " + t.getKey().method.name + " " +
|
||||
t.getKey().descriptor + " " + t.getValue().type + " " + t.getValue().name + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,7 +400,7 @@ public class JBRApiPlugin implements Plugin {
|
||||
|
||||
@Override
|
||||
public void init(JavacTask jt, String... args) {
|
||||
Path output = Path.of(args[0]);
|
||||
Path pubPath = Path.of(args[0] + ".public"), privPath = Path.of(args[0] + ".private");
|
||||
String implVersion;
|
||||
try {
|
||||
implVersion = Files.readString(Path.of(args[1])).strip();
|
||||
@@ -426,18 +427,21 @@ public class JBRApiPlugin implements Plugin {
|
||||
}
|
||||
}.scan(te.getTypeElement(), te.getCompilationUnit());
|
||||
} else if (te.getKind() == TaskEvent.Kind.COMPILATION) {
|
||||
try (RandomAccessFile file = new RandomAccessFile(output.toFile(), "rw");
|
||||
FileChannel channel = file.getChannel()) {
|
||||
try (RandomAccessFile pub = new RandomAccessFile(pubPath.toFile(), "rw");
|
||||
RandomAccessFile priv = new RandomAccessFile(privPath.toFile(), "rw");
|
||||
FileChannel channel = pub.getChannel()) {
|
||||
for (;;) {
|
||||
try { if (channel.lock() != null) break; } catch (OverlappingFileLockException ignore) {}
|
||||
LockSupport.parkNanos(10_000000);
|
||||
}
|
||||
Registry r = new Registry();
|
||||
r.read(file);
|
||||
r.read(pub, false);
|
||||
r.read(priv, true);
|
||||
var unresolvedErrors = r.addBindings();
|
||||
file.setLength(0);
|
||||
file.writeBytes("VERSION " + implVersion + "\n");
|
||||
r.write(file);
|
||||
priv.setLength(0);
|
||||
pub.setLength(0);
|
||||
pub.writeBytes("VERSION " + implVersion + "\n");
|
||||
r.write(pub, priv);
|
||||
if (!unresolvedErrors.isEmpty()) {
|
||||
throw new RuntimeException(String.join("\n", unresolvedErrors));
|
||||
}
|
||||
|
||||
@@ -45,10 +45,7 @@ public class JBRApiBootstrap {
|
||||
* @return implementation for {@link com.jetbrains.JBR.ServiceApi} interface
|
||||
*/
|
||||
public static synchronized Object bootstrap(MethodHandles.Lookup outerLookup) {
|
||||
if (!JBRApi.ENABLED) return null;
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.out.println("JBR API bootstrap in compatibility mode: Object bootstrap(MethodHandles.Lookup)");
|
||||
}
|
||||
System.out.println("JBR API bootstrap in compatibility mode: Object bootstrap(MethodHandles.Lookup)");
|
||||
Class<?> apiInterface;
|
||||
try {
|
||||
apiInterface = outerLookup.findClass("com.jetbrains.JBR$ServiceApi");
|
||||
|
||||
@@ -52,22 +52,20 @@ public class JBRApiSupport {
|
||||
* @param extensionExtractor receives method, returns its extension enum, or null
|
||||
* @return implementation for {@code JBR.ServiceApi} interface
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static synchronized Object bootstrap(Class<?> apiInterface,
|
||||
Class<? extends Annotation> serviceAnnotation,
|
||||
Class<? extends Annotation> providedAnnotation,
|
||||
Class<? extends Annotation> providesAnnotation,
|
||||
Map<Enum<?>, Class[]> knownExtensions,
|
||||
Map<Enum<?>, Class<?>[]> knownExtensions,
|
||||
Function<Method, Enum<?>> extensionExtractor) {
|
||||
if (!JBRApi.ENABLED) return null;
|
||||
JBRApi.init(
|
||||
return JBRApi.init(
|
||||
null,
|
||||
apiInterface,
|
||||
serviceAnnotation,
|
||||
providedAnnotation,
|
||||
providesAnnotation,
|
||||
knownExtensions,
|
||||
extensionExtractor);
|
||||
return JBRApi.getService(apiInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,21 +46,29 @@ import static com.jetbrains.internal.jbrapi.BytecodeUtils.*;
|
||||
*/
|
||||
class AccessContext {
|
||||
|
||||
static final int DYNAMIC_CALL_TARGET_NAME_OFFSET = 128;
|
||||
@SuppressWarnings("unchecked")
|
||||
static Supplier<MethodHandle>[] getDynamicCallTargets(Lookup target) {
|
||||
try {
|
||||
return (Supplier<MethodHandle>[]) target.findStaticVarHandle(
|
||||
target.lookupClass(), "dynamicCallTargets", Supplier[].class).get();
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
private static final DirectMethodHandleDesc BOOTSTRAP_DYNAMIC_DESC = MethodHandleDesc.ofMethod(
|
||||
DirectMethodHandleDesc.Kind.STATIC, desc(JBRApiSupport.class), "bootstrapDynamic",
|
||||
desc(CallSite.class, Lookup.class, String.class, MethodType.class));
|
||||
|
||||
private final Map<Class<?>, Boolean> accessibleClasses = new HashMap<>();
|
||||
final Map<Proxy, Boolean> dependencies = new HashMap<>(); // true for required, false for optional
|
||||
final List<DynamicCallTarget> dynamicCallTargets = new ArrayList<>();
|
||||
final List<Supplier<MethodHandle>> dynamicCallTargets = new ArrayList<>();
|
||||
final Lookup caller;
|
||||
|
||||
AccessContext(Lookup caller) {
|
||||
this.caller = caller;
|
||||
}
|
||||
|
||||
record DynamicCallTarget(String name, MethodTypeDesc descriptor, Supplier<MethodHandle> futureHandle) {}
|
||||
|
||||
class Method {
|
||||
final CodeBuilder writer;
|
||||
private final boolean methodRequired;
|
||||
@@ -84,10 +92,9 @@ class AccessContext {
|
||||
}
|
||||
|
||||
void invokeDynamic(MethodType type, Supplier<MethodHandle> futureHandle) {
|
||||
MethodTypeDesc desc = desc(erase(type));
|
||||
DynamicCallTarget t = new DynamicCallTarget("dynamic" + dynamicCallTargets.size(), desc, futureHandle);
|
||||
dynamicCallTargets.add(t);
|
||||
writer.invokedynamic(DynamicCallSiteDesc.of(BOOTSTRAP_DYNAMIC_DESC, t.name, desc));
|
||||
String name = String.valueOf((char) (dynamicCallTargets.size() + DYNAMIC_CALL_TARGET_NAME_OFFSET));
|
||||
dynamicCallTargets.add(futureHandle);
|
||||
writer.invokedynamic(DynamicCallSiteDesc.of(BOOTSTRAP_DYNAMIC_DESC, name, desc(erase(type))));
|
||||
}
|
||||
|
||||
void invokeDirect(MethodHandleInfo handleInfo) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.lang.classfile.ClassFile.ACC_FINAL;
|
||||
import static java.lang.classfile.ClassFile.ACC_PUBLIC;
|
||||
@@ -49,6 +50,8 @@ class BytecodeUtils {
|
||||
public static final ClassDesc VOID_DESC = desc(void.class);
|
||||
public static final ClassDesc OBJECT_DESC = desc(Object.class);
|
||||
public static final ClassDesc OBJECT_ARRAY_DESC = OBJECT_DESC.arrayType();
|
||||
public static final ClassDesc SUPPLIER_DESC = desc(Supplier.class);
|
||||
public static final ClassDesc SUPPLIER_ARRAY_DESC = SUPPLIER_DESC.arrayType();
|
||||
public static final ClassDesc EXTENSION_ARRAY_DESC = desc(long[].class);
|
||||
public static final ClassDesc PROXY_INTERFACE_DESC = desc(com.jetbrains.exported.JBRApiSupport.Proxy.class);
|
||||
public static final MethodTypeDesc GET_PROXY_TARGET_DESC = MethodTypeDesc.of(OBJECT_DESC);
|
||||
|
||||
@@ -33,10 +33,7 @@ import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.lang.invoke.MethodHandles.Lookup;
|
||||
|
||||
@@ -50,11 +47,13 @@ import static java.lang.invoke.MethodHandles.Lookup;
|
||||
* This class is an entry point into JBR API backend.
|
||||
* @see Proxy
|
||||
*/
|
||||
// Root is not considered a service for proxy generation purposes, as its instantiation follows custom rules.
|
||||
@Provides("JBR.ServiceApi")
|
||||
public class JBRApi {
|
||||
/**
|
||||
* Enable JBR API, it wouldn't init when disabled. Enabled by default.
|
||||
*/
|
||||
public static final boolean ENABLED = Utils.property("jetbrains.runtime.api.enabled", true);
|
||||
private static final boolean ENABLED = Utils.property("jetbrains.runtime.api.enabled", true);
|
||||
/**
|
||||
* Enable API extensions. When disabled, extension methods are treated like any other method,
|
||||
* {@link JBRApi#isExtensionSupported} always returns false, {@link JBRApi#getService(Class, Enum[])}
|
||||
@@ -64,7 +63,7 @@ public class JBRApi {
|
||||
/**
|
||||
* Enable extensive debugging logging. Disabled by default.
|
||||
*/
|
||||
public static final boolean VERBOSE = Utils.property("jetbrains.runtime.api.verbose", false);
|
||||
static final boolean VERBOSE = Utils.property("jetbrains.runtime.api.verbose", false);
|
||||
/**
|
||||
* Print warnings about usage of deprecated interfaces and methods to {@link System#err}. Enabled by default.
|
||||
*/
|
||||
@@ -78,54 +77,68 @@ public class JBRApi {
|
||||
*/
|
||||
private static final boolean EXTEND_REGISTRY = Utils.property("jetbrains.runtime.api.extendRegistry", false);
|
||||
|
||||
record DynamicCallTargetKey(Class<?> proxy, String name, String descriptor) {}
|
||||
static final ConcurrentMap<DynamicCallTargetKey, Supplier<MethodHandle>> dynamicCallTargets = new ConcurrentHashMap<>();
|
||||
private static final ProxyRepository proxyRepository = new ProxyRepository();
|
||||
|
||||
private static Boolean[] supportedExtensions;
|
||||
private static long[] emptyExtensionsBitfield;
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static Map<Enum<?>, Class[]> knownExtensions;
|
||||
static Function<Method, Enum<?>> extensionExtractor;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static void init(InputStream extendedRegistryStream,
|
||||
Class<? extends Annotation> serviceAnnotation,
|
||||
Class<? extends Annotation> providedAnnotation,
|
||||
Class<? extends Annotation> providesAnnotation,
|
||||
Map<Enum<?>, Class[]> knownExtensions,
|
||||
Function<Method, Enum<?>> extensionExtractor) {
|
||||
if (extendedRegistryStream != null && !EXTEND_REGISTRY) {
|
||||
throw new Error("Extending JBR API registry is not supported");
|
||||
}
|
||||
proxyRepository.init(extendedRegistryStream, serviceAnnotation, providedAnnotation, providesAnnotation);
|
||||
private final ProxyRepository proxyRepository;
|
||||
private final Boolean[] supportedExtensions;
|
||||
private final long[] emptyExtensionsBitfield;
|
||||
private final Map<Enum<?>, Class<?>[]> knownExtensions;
|
||||
|
||||
private JBRApi(ProxyRepository proxyRepository, Map<Enum<?>, Class<?>[]> knownExtensions) {
|
||||
this.proxyRepository = proxyRepository;
|
||||
if (EXTENSIONS_ENABLED) {
|
||||
JBRApi.knownExtensions = knownExtensions;
|
||||
JBRApi.extensionExtractor = extensionExtractor;
|
||||
this.knownExtensions = knownExtensions;
|
||||
supportedExtensions = new Boolean[
|
||||
knownExtensions.keySet().stream().mapToInt(Enum::ordinal).max().orElse(-1) + 1];
|
||||
emptyExtensionsBitfield = new long[(supportedExtensions.length + 63) / 64];
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println("JBR API init\n knownExtensions = " + (EXTENSIONS_ENABLED ? knownExtensions.keySet() : "DISABLED"));
|
||||
} else {
|
||||
this.knownExtensions = null;
|
||||
supportedExtensions = null;
|
||||
emptyExtensionsBitfield = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static MethodHandle bindDynamic(Lookup caller, String name, MethodType type) {
|
||||
public static Object init(InputStream extendedRegistryStream,
|
||||
Class<?> apiInterface,
|
||||
Class<? extends Annotation> serviceAnnotation,
|
||||
Class<? extends Annotation> providedAnnotation,
|
||||
Class<? extends Annotation> providesAnnotation,
|
||||
Map<Enum<?>, Class<?>[]> knownExtensions,
|
||||
Function<Method, Enum<?>> extensionExtractor) {
|
||||
if (!ENABLED) return null;
|
||||
if (VERBOSE) {
|
||||
System.out.println("Binding call site " + caller.lookupClass().getName() + "#" + name + ": " + type);
|
||||
System.out.println("JBR API init\n knownExtensions = " + (EXTENSIONS_ENABLED ? knownExtensions.keySet() : "DISABLED"));
|
||||
}
|
||||
|
||||
ProxyRepository.Registry registry;
|
||||
if (extendedRegistryStream != null) {
|
||||
if (!EXTEND_REGISTRY) throw new Error("Extending JBR API registry is not supported");
|
||||
registry = new ProxyRepository.Registry(extendedRegistryStream);
|
||||
} else registry = ProxyRepository.Registry.Builtin.PUBLIC;
|
||||
|
||||
ProxyRepository proxyRepository = new ProxyRepository(registry, apiInterface.getClassLoader(),
|
||||
serviceAnnotation, providedAnnotation, providesAnnotation, extensionExtractor);
|
||||
JBRApi api = new JBRApi(proxyRepository, knownExtensions);
|
||||
|
||||
try {
|
||||
Proxy p = proxyRepository.getProxy(apiInterface, null);
|
||||
if (!p.init()) throw new Error("Proxy initialization failed");
|
||||
MethodHandle constructor = p.getConstructor();
|
||||
return EXTENSIONS_ENABLED ? constructor.invoke(api, api.emptyExtensionsBitfield) : constructor.invoke(api);
|
||||
} catch (Throwable e) {
|
||||
if (VERBOSE) {
|
||||
synchronized (System.err) {
|
||||
Utils.log(Utils.BEFORE_JBR, System.err, "Warning: JBR API is not supported");
|
||||
System.err.print("Caused by: ");
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!caller.hasFullPrivilegeAccess()) throw new Error("Caller lookup must have full privilege access"); // Authenticity check.
|
||||
return dynamicCallTargets.get(new DynamicCallTargetKey(caller.lookupClass(), name, type.descriptorString())).get().asType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JBR API version supported by current implementation.
|
||||
*/
|
||||
@Provides("JBR.ServiceApi")
|
||||
public static String getImplVersion() {
|
||||
public String getImplVersion() {
|
||||
return proxyRepository.getVersion();
|
||||
}
|
||||
|
||||
@@ -135,8 +148,7 @@ public class JBRApi {
|
||||
* @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
|
||||
* service, but is not directly exposed to user.
|
||||
*/
|
||||
@Provides("JBR.ServiceApi")
|
||||
public static boolean isExtensionSupported(Enum<?> extension) {
|
||||
public boolean isExtensionSupported(Enum<?> extension) {
|
||||
if (!EXTENSIONS_ENABLED) return false;
|
||||
int i = extension.ordinal();
|
||||
if (supportedExtensions[i] == null) {
|
||||
@@ -153,30 +165,13 @@ public class JBRApi {
|
||||
return supportedExtensions[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return fully supported service implementation for the given interface with specified extensions, or null
|
||||
* @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
|
||||
* service, but is not directly exposed to user.
|
||||
*/
|
||||
@Provides("JBR.ServiceApi")
|
||||
public static <T> T getService(Class<T> interFace, Enum<?>... extensions) {
|
||||
if (!EXTENSIONS_ENABLED) return getService(interFace);
|
||||
|
||||
long[] bitfield = new long[emptyExtensionsBitfield.length];
|
||||
for (Enum<?> e : extensions) {
|
||||
if (isExtensionSupported(e)) {
|
||||
int i = e.ordinal() / 64;
|
||||
int j = e.ordinal() % 64;
|
||||
bitfield[i] |= 1L << j;
|
||||
} else {
|
||||
if (VERBOSE) {
|
||||
Utils.log(Utils.BEFORE_JBR, System.err, "Warning: Extension not supported: " + e.name());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public static <T> T getInternalService(Class<T> interFace) {
|
||||
class Holder {
|
||||
private static final JBRApi INSTANCE = new JBRApi(
|
||||
new ProxyRepository(ProxyRepository.Registry.Builtin.PRIVATE, JBRApi.class.getClassLoader(),
|
||||
null, null, null, null), Map.of());
|
||||
}
|
||||
|
||||
return getService(interFace, bitfield, true);
|
||||
return Holder.INSTANCE.getService(interFace);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,19 +179,36 @@ public class JBRApi {
|
||||
* @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
|
||||
* service, but is not directly exposed to user.
|
||||
*/
|
||||
@Provides("JBR.ServiceApi")
|
||||
public static <T> T getService(Class<T> interFace) {
|
||||
return getService(interFace, emptyExtensionsBitfield, true);
|
||||
}
|
||||
|
||||
public static <T> T getInternalService(Class<T> interFace) {
|
||||
return getService(interFace, emptyExtensionsBitfield, false);
|
||||
public <T> T getService(Class<T> interFace) {
|
||||
return getService(interFace, new Enum<?>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return fully supported service implementation for the given interface with specified extensions, or null
|
||||
* @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
|
||||
* service, but is not directly exposed to user.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T getService(Class<T> interFace, long[] extensions, boolean publicService) {
|
||||
public <T> T getService(Class<T> interFace, Enum<?>... extensions) {
|
||||
long[] bitfield;
|
||||
if (extensions.length > 0 && EXTENSIONS_ENABLED) {
|
||||
bitfield = new long[emptyExtensionsBitfield.length];
|
||||
for (Enum<?> e : extensions) {
|
||||
if (isExtensionSupported(e)) {
|
||||
int i = e.ordinal() / 64;
|
||||
int j = e.ordinal() % 64;
|
||||
bitfield[i] |= 1L << j;
|
||||
} else {
|
||||
if (VERBOSE) {
|
||||
Utils.log(Utils.BEFORE_JBR, System.err, "Warning: Extension not supported: " + e.name());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else bitfield = emptyExtensionsBitfield;
|
||||
|
||||
Proxy p = proxyRepository.getProxy(interFace, null);
|
||||
if ((p.getFlags() & Proxy.SERVICE) == 0 || (publicService && (p.getFlags() & Proxy.INTERNAL) != 0)) {
|
||||
if ((p.getFlags() & Proxy.SERVICE) == 0) {
|
||||
if (VERBOSE) {
|
||||
Utils.log(Utils.BEFORE_JBR, System.err, "Warning: Not allowed as a service: " + interFace.getCanonicalName());
|
||||
}
|
||||
@@ -210,7 +222,7 @@ public class JBRApi {
|
||||
}
|
||||
try {
|
||||
MethodHandle constructor = p.getConstructor();
|
||||
return (T) (EXTENSIONS_ENABLED ? constructor.invoke(extensions) : constructor.invoke());
|
||||
return (T) (EXTENSIONS_ENABLED ? constructor.invoke(bitfield) : constructor.invoke());
|
||||
} catch (com.jetbrains.exported.JBRApi.ServiceNotAvailableException | NullPointerException e) {
|
||||
if (VERBOSE) {
|
||||
synchronized (System.err) {
|
||||
@@ -224,4 +236,15 @@ public class JBRApi {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static MethodHandle bindDynamic(Lookup caller, String name, MethodType type) {
|
||||
int index = name.charAt(0) - AccessContext.DYNAMIC_CALL_TARGET_NAME_OFFSET;
|
||||
if (VERBOSE) {
|
||||
System.out.println("Binding call site " + caller.lookupClass().getName() + " #" + index);
|
||||
}
|
||||
if (!caller.hasFullPrivilegeAccess()) {
|
||||
throw new Error("Caller lookup must have full privilege access"); // Authenticity check.
|
||||
}
|
||||
return AccessContext.getDynamicCallTargets(caller)[index].get().asType(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +96,7 @@ class Proxy {
|
||||
/**
|
||||
* @see Proxy.Info#flags
|
||||
*/
|
||||
static final int
|
||||
INTERNAL = 1,
|
||||
SERVICE = 2;
|
||||
static final int SERVICE = 1;
|
||||
|
||||
private final Proxy inverse;
|
||||
private final Class<?> interFace, target;
|
||||
|
||||
@@ -75,6 +75,7 @@ class ProxyGenerator {
|
||||
return (method.getModifiers() & (Modifier.STATIC | Modifier.FINAL)) == 0;
|
||||
}
|
||||
|
||||
private final ProxyRepository proxyRepository;
|
||||
private final Proxy.Info info;
|
||||
private final Class<?> interFace;
|
||||
private final Lookup proxyGenLookup;
|
||||
@@ -98,6 +99,7 @@ class ProxyGenerator {
|
||||
* Creates new proxy generator from given {@link Proxy.Info},
|
||||
*/
|
||||
ProxyGenerator(ProxyRepository proxyRepository, Proxy.Info info, Mapping[] specialization) {
|
||||
this.proxyRepository = proxyRepository;
|
||||
this.info = info;
|
||||
this.interFace = info.interfaceLookup.lookupClass();
|
||||
this.specialization = specialization;
|
||||
@@ -193,10 +195,10 @@ class ProxyGenerator {
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.out.println("Initializing proxy " + interFace.getName());
|
||||
}
|
||||
for (var t : accessContext.dynamicCallTargets) {
|
||||
JBRApi.dynamicCallTargets.put(new JBRApi.DynamicCallTargetKey(
|
||||
generatedProxy.lookupClass(), t.name(), t.descriptor().descriptorString()
|
||||
), t.futureHandle());
|
||||
if (!accessContext.dynamicCallTargets.isEmpty()) {
|
||||
var table = AccessContext.getDynamicCallTargets(generatedProxy);
|
||||
for (int i = 0; i < table.length; i++) table[i] = accessContext.dynamicCallTargets.get(i);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +210,7 @@ class ProxyGenerator {
|
||||
try {
|
||||
Object sericeTarget = service && info.targetLookup != null ? createServiceTarget() : null;
|
||||
generatedProxy = proxyGenLookup.defineHiddenClass(
|
||||
bytecode, false, Lookup.ClassOption.STRONG, Lookup.ClassOption.NESTMATE);
|
||||
bytecode, false, Lookup.ClassOption.NESTMATE);
|
||||
MethodHandle constructor = findConstructor();
|
||||
if (sericeTarget != null) constructor = MethodHandles.insertArguments(constructor, 0, sericeTarget);
|
||||
return constructor;
|
||||
@@ -242,10 +244,10 @@ class ProxyGenerator {
|
||||
cb.withFlags(ACC_SUPER | ACC_FINAL | ACC_SYNTHETIC)
|
||||
.withSuperclass(superclassDesc)
|
||||
.withInterfaceSymbols(superinterfaceDescs);
|
||||
generateFields(cb);
|
||||
generateConstructor(cb);
|
||||
generateTargetGetter(cb);
|
||||
generateMethods(cb);
|
||||
generateFields(cb);
|
||||
});
|
||||
if (JBRApi.VERIFY_BYTECODE) {
|
||||
List<VerifyError> errors = ClassFile.of().verify(bytecode);
|
||||
@@ -265,6 +267,14 @@ class ProxyGenerator {
|
||||
if (EXTENSIONS_ENABLED) {
|
||||
cb.withField("extensions", EXTENSION_ARRAY_DESC, ACC_PRIVATE | ACC_FINAL);
|
||||
}
|
||||
if (!accessContext.dynamicCallTargets.isEmpty()) {
|
||||
cb.withField("dynamicCallTargets", SUPPLIER_ARRAY_DESC, ACC_PRIVATE | ACC_FINAL | ACC_STATIC);
|
||||
cb.withMethodBody("<clinit>", MethodTypeDesc.of(VOID_DESC), ACC_PRIVATE | ACC_STATIC, m -> m
|
||||
.loadConstant(accessContext.dynamicCallTargets.size())
|
||||
.anewarray(SUPPLIER_DESC)
|
||||
.putstatic(proxyDesc, "dynamicCallTargets", SUPPLIER_ARRAY_DESC)
|
||||
.return_());
|
||||
}
|
||||
}
|
||||
|
||||
private void generateConstructor(ClassBuilder cb) {
|
||||
@@ -325,8 +335,8 @@ class ProxyGenerator {
|
||||
|
||||
private void generateMethod(ClassBuilder cb, Method method) {
|
||||
Exception exception = null;
|
||||
Enum<?> extension = EXTENSIONS_ENABLED && JBRApi.extensionExtractor != null ?
|
||||
JBRApi.extensionExtractor.apply(method) : null;
|
||||
Enum<?> extension = EXTENSIONS_ENABLED && proxyRepository.extensionExtractor != null ?
|
||||
proxyRepository.extensionExtractor.apply(method) : null;
|
||||
Mapping.Method methodMapping = mappingContext.getMapping(method);
|
||||
MethodHandle handle;
|
||||
boolean passInstance;
|
||||
|
||||
@@ -35,8 +35,10 @@ import java.io.InputStreamReader;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.lang.invoke.MethodHandles.Lookup;
|
||||
@@ -48,15 +50,26 @@ import static java.lang.invoke.MethodHandles.Lookup;
|
||||
class ProxyRepository {
|
||||
private static final Proxy NONE = Proxy.empty(null), INVALID = Proxy.empty(false);
|
||||
|
||||
private final Registry registry = new Registry();
|
||||
private final Map<Key, Proxy> proxies = new HashMap<>();
|
||||
private final Registry registry;
|
||||
private final ClassLoader classLoader;
|
||||
private final Class<? extends Annotation> serviceAnnotation, providedAnnotation, providesAnnotation;
|
||||
private final Module annotationsModule;
|
||||
final Function<Method, Enum<?>> extensionExtractor;
|
||||
|
||||
void init(InputStream extendedRegistryStream,
|
||||
Class<? extends Annotation> serviceAnnotation,
|
||||
Class<? extends Annotation> providedAnnotation,
|
||||
Class<? extends Annotation> providesAnnotation) {
|
||||
registry.initAnnotations(serviceAnnotation, providedAnnotation, providesAnnotation);
|
||||
if (extendedRegistryStream != null) registry.readEntries(extendedRegistryStream);
|
||||
ProxyRepository(Registry registry,
|
||||
ClassLoader classLoader,
|
||||
Class<? extends Annotation> serviceAnnotation,
|
||||
Class<? extends Annotation> providedAnnotation,
|
||||
Class<? extends Annotation> providesAnnotation,
|
||||
Function<Method, Enum<?>> extensionExtractor) {
|
||||
this.registry = registry;
|
||||
this.classLoader = classLoader;
|
||||
this.serviceAnnotation = serviceAnnotation;
|
||||
this.providedAnnotation = providedAnnotation;
|
||||
this.providesAnnotation = providesAnnotation;
|
||||
this.extensionExtractor = extensionExtractor;
|
||||
annotationsModule = serviceAnnotation == null ? null : serviceAnnotation.getModule();
|
||||
}
|
||||
|
||||
String getVersion() {
|
||||
@@ -67,15 +80,14 @@ class ProxyRepository {
|
||||
Key key = new Key(clazz, specialization);
|
||||
Proxy p = proxies.get(key);
|
||||
if (p == null) {
|
||||
registry.updateClassLoader(clazz.getClassLoader());
|
||||
Mapping[] inverseSpecialization = specialization == null ? null :
|
||||
Stream.of(specialization).map(m -> m == null ? null : m.inverse()).toArray(Mapping[]::new);
|
||||
Key inverseKey = null;
|
||||
|
||||
Registry.Entry entry = registry.entries.get(key.clazz().getCanonicalName());
|
||||
if (entry != null) { // This is a registered proxy
|
||||
Proxy.Info infoByInterface = entry.resolve(),
|
||||
infoByTarget = entry.inverse != null ? entry.inverse.resolve() : null;
|
||||
Proxy.Info infoByInterface = entry.resolve(this),
|
||||
infoByTarget = entry.inverse != null ? entry.inverse.resolve(this) : null;
|
||||
inverseKey = infoByTarget != null && infoByTarget.interfaceLookup != null ?
|
||||
new Key(infoByTarget.interfaceLookup.lookupClass(), inverseSpecialization) : null;
|
||||
if ((infoByInterface == null && infoByTarget == null) ||
|
||||
@@ -123,9 +135,9 @@ class ProxyRepository {
|
||||
|
||||
/**
|
||||
* Registry contains all information about mapping between JBR API interfaces and implementation.
|
||||
* This mapping information can be {@linkplain Entry#resolve() resolved} into {@link Proxy.Info}.
|
||||
* This mapping information can be {@linkplain Entry#resolve(ProxyRepository) resolved} into {@link Proxy.Info}.
|
||||
*/
|
||||
private static class Registry {
|
||||
static class Registry {
|
||||
|
||||
private record StaticKey(String methodName, String targetMethodDescriptor) {}
|
||||
private record StaticValue(String targetType, String targetMethodName) {}
|
||||
@@ -140,12 +152,12 @@ class ProxyRepository {
|
||||
|
||||
private Entry(String type) { this.type = type; }
|
||||
|
||||
private Proxy.Info resolve() {
|
||||
private Proxy.Info resolve(ProxyRepository repository) {
|
||||
if (type == null) return null;
|
||||
Lookup l, t;
|
||||
try {
|
||||
l = resolveType(type, classLoader);
|
||||
t = target != null ? resolveType(target, classLoader) : null;
|
||||
l = resolveType(type, repository.classLoader);
|
||||
t = target != null ? resolveType(target, repository.classLoader) : null;
|
||||
} catch (ClassNotFoundException e) {
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.err.println(type + " not eligible");
|
||||
@@ -166,18 +178,18 @@ class ProxyRepository {
|
||||
return INVALID;
|
||||
}
|
||||
if (target == null) flags |= Proxy.SERVICE;
|
||||
if (needsAnnotation(l.lookupClass())) {
|
||||
if (!hasAnnotation(l.lookupClass(), providedAnnotation)) {
|
||||
if (needsAnnotation(repository, l.lookupClass())) {
|
||||
if (!hasAnnotation(l.lookupClass(), repository.providedAnnotation)) {
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.err.println(type + " not eligible: no @Provided annotation");
|
||||
}
|
||||
return INVALID;
|
||||
}
|
||||
if (!hasAnnotation(l.lookupClass(), serviceAnnotation)) flags &= ~Proxy.SERVICE;
|
||||
if (!hasAnnotation(l.lookupClass(), repository.serviceAnnotation)) flags &= ~Proxy.SERVICE;
|
||||
}
|
||||
Proxy.Info info;
|
||||
if (t != null) {
|
||||
if (needsAnnotation(t.lookupClass()) && !hasAnnotation(t.lookupClass(), providesAnnotation)) {
|
||||
if (needsAnnotation(repository, t.lookupClass()) && !hasAnnotation(t.lookupClass(), repository.providesAnnotation)) {
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.err.println(target + " not eligible: no @Provides annotation");
|
||||
}
|
||||
@@ -191,8 +203,8 @@ class ProxyRepository {
|
||||
String targetType = method.getValue().targetType;
|
||||
String targetMethodName = method.getValue().targetMethodName;
|
||||
try {
|
||||
Lookup lookup = resolveType(targetType, classLoader);
|
||||
MethodType mt = MethodType.fromMethodDescriptorString(targetMethodDescriptor, classLoader);
|
||||
Lookup lookup = resolveType(targetType, repository.classLoader);
|
||||
MethodType mt = MethodType.fromMethodDescriptorString(targetMethodDescriptor, repository.classLoader);
|
||||
MethodHandle handle = lookup.findStatic(lookup.lookupClass(), targetMethodName, mt);
|
||||
info.addStaticMethod(methodName, handle);
|
||||
} catch (ClassNotFoundException | IllegalArgumentException | TypeNotPresentException |
|
||||
@@ -230,38 +242,41 @@ class ProxyRepository {
|
||||
public String toString() { return type; }
|
||||
}
|
||||
|
||||
private Class<? extends Annotation> serviceAnnotation, providedAnnotation, providesAnnotation;
|
||||
private Module annotationsModule;
|
||||
private ClassLoader classLoader;
|
||||
private final Map<String, Entry> entries = new HashMap<>();
|
||||
private final String version;
|
||||
|
||||
private Registry() {
|
||||
try (InputStream registryStream = BootLoader.findResourceAsStream("java.base", "META-INF/jbrapi.registry")) {
|
||||
version = readEntries(registryStream);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
static class Builtin {
|
||||
static final Registry PRIVATE, PUBLIC;
|
||||
static {
|
||||
try {
|
||||
PRIVATE = new Registry(BootLoader.findResourceAsStream("java.base", "META-INF/jbrapi.private"));
|
||||
PUBLIC = new Registry(BootLoader.findResourceAsStream("java.base", "META-INF/jbrapi.public"));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initAnnotations(Class<? extends Annotation> serviceAnnotation,
|
||||
Class<? extends Annotation> providedAnnotation,
|
||||
Class<? extends Annotation> providesAnnotation) {
|
||||
this.serviceAnnotation = serviceAnnotation;
|
||||
this.providedAnnotation = providedAnnotation;
|
||||
this.providesAnnotation = providesAnnotation;
|
||||
annotationsModule = serviceAnnotation == null ? null : serviceAnnotation.getModule();
|
||||
if (annotationsModule != null) classLoader = annotationsModule.getClassLoader();
|
||||
}
|
||||
private final Map<String, Entry> entries = new HashMap<>();
|
||||
private final String version;
|
||||
|
||||
private String readEntries(InputStream inputStream) {
|
||||
Registry(InputStream inputStream) {
|
||||
String version = null;
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
String s;
|
||||
while ((s = reader.readLine()) != null) {
|
||||
String[] tokens = s.split(" ");
|
||||
switch (tokens[0]) {
|
||||
case "TYPE" -> {
|
||||
case "VERSION" -> version = tokens[1];
|
||||
case "STATIC" -> {
|
||||
Entry entry = entries.computeIfAbsent(tokens[4], Entry::new);
|
||||
StaticValue target = new StaticValue(tokens[1], tokens[2]);
|
||||
StaticValue prev = entry.staticMethods.put(new StaticKey(tokens[5], tokens[3]), target);
|
||||
if (prev != null && !prev.equals(target)) {
|
||||
throw new RuntimeException("Conflicting mapping: " +
|
||||
target.targetType + "#" + target.targetMethodName + " <- " +
|
||||
tokens[4] + "#" + tokens[5] + " -> " +
|
||||
prev.targetType + "#" + prev.targetMethodName);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
Entry a = entries.computeIfAbsent(tokens[1], Entry::new);
|
||||
Entry b = entries.computeIfAbsent(tokens[2], Entry::new);
|
||||
if ((a.inverse != null || b.inverse != null) && (a.inverse != b || b.inverse != a)) {
|
||||
@@ -274,7 +289,7 @@ class ProxyRepository {
|
||||
b.inverse = a;
|
||||
a.target = tokens[2];
|
||||
b.target = tokens[1];
|
||||
switch (tokens[3]) {
|
||||
switch (tokens[0]) {
|
||||
case "SERVICE" -> {
|
||||
a.type = null;
|
||||
b.flags |= Proxy.SERVICE;
|
||||
@@ -282,56 +297,17 @@ class ProxyRepository {
|
||||
case "PROVIDES" -> a.type = null;
|
||||
case "PROVIDED" -> b.type = null;
|
||||
}
|
||||
if (tokens.length > 4 && tokens[4].equals("INTERNAL")) {
|
||||
a.flags |= Proxy.INTERNAL;
|
||||
b.flags |= Proxy.INTERNAL;
|
||||
}
|
||||
}
|
||||
case "STATIC" -> {
|
||||
Entry entry = entries.computeIfAbsent(tokens[4], Entry::new);
|
||||
StaticValue target = new StaticValue(tokens[1], tokens[2]);
|
||||
StaticValue prev = entry.staticMethods.put(new StaticKey(tokens[5], tokens[3]), target);
|
||||
if (prev != null && !prev.equals(target)) {
|
||||
throw new RuntimeException("Conflicting mapping: " +
|
||||
target.targetType + "#" + target.targetMethodName + " <- " +
|
||||
tokens[4] + "#" + tokens[5] + " -> " +
|
||||
prev.targetType + "#" + prev.targetMethodName);
|
||||
}
|
||||
if (tokens.length > 6 && tokens[6].equals("INTERNAL")) entry.flags |= Proxy.INTERNAL;
|
||||
}
|
||||
case "VERSION" -> version = tokens[1];
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
entries.clear();
|
||||
throw new RuntimeException(e);
|
||||
} catch (RuntimeException | Error e) {
|
||||
entries.clear();
|
||||
throw e;
|
||||
}
|
||||
return version;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
private synchronized void updateClassLoader(ClassLoader newLoader) {
|
||||
// New loader is descendant of current one -> update
|
||||
for (ClassLoader cl = newLoader;; cl = cl.getParent()) {
|
||||
if (cl == classLoader) {
|
||||
classLoader = newLoader;
|
||||
return;
|
||||
}
|
||||
if (cl == null) break;
|
||||
}
|
||||
// Current loader is descendant of the new one -> leave
|
||||
for (ClassLoader cl = classLoader;; cl = cl.getParent()) {
|
||||
if (cl == newLoader) return;
|
||||
if (cl == null) break;
|
||||
}
|
||||
// Independent classloaders -> error? Or maybe reset cache and start fresh?
|
||||
throw new RuntimeException("Incompatible classloader");
|
||||
}
|
||||
|
||||
private boolean needsAnnotation(Class<?> c) {
|
||||
return annotationsModule != null && annotationsModule.equals(c.getModule());
|
||||
private boolean needsAnnotation(ProxyRepository repository, Class<?> c) {
|
||||
return repository.annotationsModule != null && repository.annotationsModule.equals(c.getModule());
|
||||
}
|
||||
|
||||
private static boolean hasAnnotation(Class<?> c, Class<? extends Annotation> a) {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2026 JetBrains s.r.o.
|
||||
* 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 sun.awt.wl.im.text_input_unstable_v3;
|
||||
|
||||
import sun.awt.UNIXToolkit;
|
||||
|
||||
import java.awt.Toolkit;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
final class CurrentDesktopInfo {
|
||||
|
||||
private CurrentDesktopInfo() {}
|
||||
|
||||
|
||||
public static boolean isGnome() {
|
||||
Boolean result = isGnome.get();
|
||||
|
||||
if (result == null) {
|
||||
synchronized (CurrentDesktopInfo.class) {
|
||||
if (Toolkit.getDefaultToolkit() instanceof UNIXToolkit unixToolkit) {
|
||||
result = "gnome".equals(unixToolkit.getDesktop());
|
||||
} else {
|
||||
result = false;
|
||||
}
|
||||
|
||||
isGnome.set(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
// {@code null} if not initialized yet
|
||||
private static final AtomicReference<Boolean> isGnome = new AtomicReference<>(null);
|
||||
|
||||
/** negative if couldn't obtain or the desktop is not GNOME */
|
||||
public static int getGnomeShellMajorVersion() {
|
||||
Integer result = gnomeVersion.get();
|
||||
|
||||
if (result == null) {
|
||||
synchronized (CurrentDesktopInfo.class) {
|
||||
if (!isGnome()) {
|
||||
result = -1;
|
||||
} else if (Toolkit.getDefaultToolkit() instanceof UNIXToolkit unixToolkit) {
|
||||
try {
|
||||
result = Objects.requireNonNullElse(unixToolkit.getGnomeShellMajorVersion(), -1);
|
||||
} catch (Exception ignored) {
|
||||
result = -1;
|
||||
}
|
||||
} else {
|
||||
result = -1;
|
||||
}
|
||||
|
||||
gnomeVersion.set(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
// {@code null} if not initialized yet, negative if couldn't obtain or the desktop is not GNOME
|
||||
private static final AtomicReference<Integer> gnomeVersion = new AtomicReference<>(null);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 JetBrains s.r.o.
|
||||
* Copyright 2025-2026 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
package sun.awt.wl.im.text_input_unstable_v3;
|
||||
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -36,6 +37,25 @@ import java.util.Objects;
|
||||
*/
|
||||
final class IncomingChanges
|
||||
{
|
||||
public static class ConversionException extends java.io.IOException {
|
||||
@java.io.Serial
|
||||
private static final long serialVersionUID = 7010594789107134519L;
|
||||
|
||||
public ConversionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public ConversionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
public ConversionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
public ConversionException(Throwable cause, String format, Object... args) {
|
||||
super(String.format(format, args), cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IncomingChanges updatePreeditString(byte[] newPreeditStringUtf8, int newPreeditStringCursorBeginUtf8Byte, int newPreeditStringCursorEndUtf8Byte) {
|
||||
this.doUpdatePreeditString = true;
|
||||
this.newPreeditStringUtf8 = newPreeditStringUtf8;
|
||||
@@ -50,18 +70,30 @@ final class IncomingChanges
|
||||
* @return {@code null} if there are no changes in the preedit string
|
||||
* (i.e. {@link #updatePreeditString(byte[], int, int)} hasn't been called);
|
||||
* an instance of JavaPreeditString otherwise.
|
||||
* @throws ConversionException if failed to convert the data provided in {@link #updatePreeditString(byte[], int, int)}
|
||||
* to an instance of JavaPreeditString.
|
||||
* @see JavaPreeditString
|
||||
*/
|
||||
public JavaPreeditString getPreeditString() {
|
||||
public JavaPreeditString getPreeditString() throws ConversionException {
|
||||
if (cachedResultPreeditString != null) {
|
||||
return cachedResultPreeditString;
|
||||
}
|
||||
|
||||
cachedResultPreeditString = doUpdatePreeditString
|
||||
? JavaPreeditString.fromWaylandPreeditString(newPreeditStringUtf8, newPreeditStringCursorBeginUtf8Byte, newPreeditStringCursorEndUtf8Byte)
|
||||
: null;
|
||||
try {
|
||||
cachedResultPreeditString = doUpdatePreeditString
|
||||
? JavaPreeditString.fromWaylandPreeditString(newPreeditStringUtf8, newPreeditStringCursorBeginUtf8Byte, newPreeditStringCursorEndUtf8Byte)
|
||||
: null;
|
||||
|
||||
return cachedResultPreeditString;
|
||||
return cachedResultPreeditString;
|
||||
} catch (CharacterCodingException err) {
|
||||
throw new ConversionException(
|
||||
err,
|
||||
"Failed to convert zwp_text_input_v3::preedit_string(%s, %d, %d) to JavaPreeditString",
|
||||
byteArrayToHexArrayString(newPreeditStringUtf8),
|
||||
newPreeditStringCursorBeginUtf8Byte,
|
||||
newPreeditStringCursorEndUtf8Byte
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,18 +109,28 @@ final class IncomingChanges
|
||||
* @return {@code null} if there are no changes in the commit string
|
||||
* (i.e. {@link #updateCommitString(byte[])} hasn't been called);
|
||||
* an instance of JavaCommitString otherwise.
|
||||
* @throws ConversionException if failed to convert the data provided in {@link #updateCommitString(byte[])}
|
||||
* to an instance of JavaCommitString.
|
||||
* @see JavaCommitString
|
||||
*/
|
||||
public JavaCommitString getCommitString() {
|
||||
public JavaCommitString getCommitString() throws ConversionException {
|
||||
if (cachedResultCommitString != null) {
|
||||
return cachedResultCommitString;
|
||||
}
|
||||
|
||||
cachedResultCommitString = doUpdateCommitString
|
||||
? JavaCommitString.fromWaylandCommitString(newCommitStringUtf8)
|
||||
: null;
|
||||
try {
|
||||
cachedResultCommitString = doUpdateCommitString
|
||||
? JavaCommitString.fromWaylandCommitString(newCommitStringUtf8)
|
||||
: null;
|
||||
|
||||
return cachedResultCommitString;
|
||||
return cachedResultCommitString;
|
||||
} catch (CharacterCodingException err) {
|
||||
throw new ConversionException(
|
||||
err,
|
||||
"Failed to convert zwp_text_input_v3::commit_string(%s) to JavaCommitString",
|
||||
byteArrayToHexArrayString(newCommitStringUtf8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,4 +170,27 @@ final class IncomingChanges
|
||||
private boolean doUpdateCommitString = false;
|
||||
private byte[] newCommitStringUtf8 = null;
|
||||
private JavaCommitString cachedResultCommitString = null;
|
||||
|
||||
|
||||
private static String byteArrayToHexArrayString(byte[] arr) {
|
||||
if (arr == null)
|
||||
return "null";
|
||||
|
||||
final int iMax = Math.min(arr.length - 1, 15);
|
||||
if (iMax == -1)
|
||||
return "[]";
|
||||
|
||||
final StringBuilder b = new StringBuilder();
|
||||
b.append('[');
|
||||
for (int i = 0; ; ++i) {
|
||||
b.append("0x").append(Integer.toHexString(Byte.toUnsignedInt(arr[i])));
|
||||
if (i == iMax) {
|
||||
if (iMax < arr.length - 1) {
|
||||
b.append(", ...");
|
||||
}
|
||||
return b.append(']').toString();
|
||||
}
|
||||
b.append(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 JetBrains s.r.o.
|
||||
* Copyright 2025-2026 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -25,6 +25,13 @@
|
||||
|
||||
package sun.awt.wl.im.text_input_unstable_v3;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CoderResult;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
record JavaCommitString(String text) {
|
||||
@@ -34,8 +41,39 @@ record JavaCommitString(String text) {
|
||||
|
||||
public static final JavaCommitString EMPTY = new JavaCommitString("");
|
||||
|
||||
/** Never returns {@code null}. */
|
||||
public static JavaCommitString fromWaylandCommitString(byte[] utf8Bytes) {
|
||||
return new JavaCommitString(Utilities.utf8BytesToJavaString(utf8Bytes));
|
||||
/**
|
||||
* Converts a UTF-8 string received in a {@code zwp_text_input_v3::commit_string} event to its UTF-16 equivalent.
|
||||
*
|
||||
* @return an instance of {@code JavaCommitString}. Never returns {@code null}.
|
||||
* @throws CharacterCodingException if {@code utf8Bytes} doesn't represent a valid UTF-8 string.
|
||||
*/
|
||||
public static JavaCommitString fromWaylandCommitString(byte[] utf8Bytes) throws CharacterCodingException {
|
||||
// The only Unicode code point that can contain zero byte(s) is U+000000.
|
||||
// It hardly makes sense to have it at the end of preedit/commit strings, so let's trim it.
|
||||
final int utf8BytesCorrectedLength = Utilities.getLengthOfUtf8BytesWithoutTrailingNULs(utf8Bytes);
|
||||
|
||||
if (utf8BytesCorrectedLength < 1) {
|
||||
return JavaCommitString.EMPTY;
|
||||
}
|
||||
|
||||
final CharsetDecoder utf8Decoder = StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
// there can't be unmappable characters for this
|
||||
// kind of conversion, so REPLACE is just in case
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
final CharBuffer decodingBuffer = CharBuffer.allocate(utf8BytesCorrectedLength + 1);
|
||||
final ByteBuffer utf8BytesBuffer = ByteBuffer.wrap(utf8Bytes, 0, utf8BytesCorrectedLength);
|
||||
|
||||
CoderResult decodingResult = utf8Decoder.decode(utf8BytesBuffer, decodingBuffer, true);
|
||||
if (decodingResult.isError() || decodingResult.isOverflow()) {
|
||||
decodingResult.throwException();
|
||||
}
|
||||
|
||||
decodingResult = utf8Decoder.flush(decodingBuffer);
|
||||
if (decodingResult.isError() || decodingResult.isOverflow()) {
|
||||
decodingResult.throwException();
|
||||
}
|
||||
|
||||
return new JavaCommitString(decodingBuffer.flip().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 JetBrains s.r.o.
|
||||
* Copyright 2025-2026 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -25,6 +25,13 @@
|
||||
|
||||
package sun.awt.wl.im.text_input_unstable_v3;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CoderResult;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -44,10 +51,11 @@ import java.util.Objects;
|
||||
* @param cursorEndCodeUnit UTF-16 equivalent of {@code preedit_string.cursor_end}.
|
||||
* It's not explicitly stated in the protocol specification, but it seems to be a valid
|
||||
* situation when cursor_end < cursor_begin, which means
|
||||
* the highlight extends to the right from the caret
|
||||
* (e.g., when the text gets selected with Shift + Left Arrow).
|
||||
* the highlight extends to the left from the caret.
|
||||
*
|
||||
* @see #fromWaylandPreeditString(byte[], int, int)
|
||||
*/
|
||||
record JavaPreeditString(String text, int cursorBeginCodeUnit, int cursorEndCodeUnit) {
|
||||
public record JavaPreeditString(String text, int cursorBeginCodeUnit, int cursorEndCodeUnit) {
|
||||
public JavaPreeditString {
|
||||
Objects.requireNonNull(text, "text");
|
||||
}
|
||||
@@ -55,13 +63,43 @@ record JavaPreeditString(String text, int cursorBeginCodeUnit, int cursorEndCode
|
||||
public static final JavaPreeditString EMPTY = new JavaPreeditString("", 0, 0);
|
||||
public static final JavaPreeditString EMPTY_NO_CARET = new JavaPreeditString("", -1, -1);
|
||||
|
||||
|
||||
/**
|
||||
* Converts a UTF-8 string and indices to it received in a {@code zwp_text_input_v3::preedit_string} event to their UTF-16 equivalents.
|
||||
* <p>
|
||||
* This is how data inconsistencies are handled:
|
||||
* <ul>
|
||||
* <li>If {@code utf8Bytes} doesn't represent a valid UTF-8 string, a {@link CharacterCodingException} is thrown.</li>
|
||||
* <li>Otherwise, if {@code cursorBeginUtf8Byte} points to a middle byte of a code point,
|
||||
* it's considered as violating the protocol specification.
|
||||
* In this case both {@link #cursorBeginCodeUnit} and {@link #cursorEndCodeUnit} of the resulting {@code JavaPreeditString}
|
||||
* will point to the end of its {@link #text}.</li>
|
||||
* <li>Also, if {@code cursorEndUtf8Byte} points to a middle byte of a code point,
|
||||
* it's considered as violating the protocol specification.
|
||||
* In this case {@link #cursorEndCodeUnit} of the resulting {@code JavaPreeditString}
|
||||
* will be set equal to its {@link #cursorBeginCodeUnit}.</li>
|
||||
* <li>If {@code cursorBeginUtf8Byte} or {@code cursorEndUtf8Byte} > {@code utf8Bytes.length},
|
||||
* the corresponding {@link #cursorBeginCodeUnit} or {@link #cursorEndCodeUnit} of the resulting {@code JavaPreeditString}
|
||||
* will be set to the length of its {@link #text}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param utf8Bytes an UTF-8 encoded string
|
||||
* @param cursorBeginUtf8Byte {@code cursor_begin} parameter of the same {@code zwp_text_input_v3::preedit_string} event
|
||||
* @param cursorEndUtf8Byte {@code cursor_end} parameter of the same {@code zwp_text_input_v3::preedit_string} event
|
||||
*
|
||||
* @return an instance of {@code JavaPreeditString}. Never returns {@code null}
|
||||
*
|
||||
* @throws CharacterCodingException if {@code utf8Bytes} doesn't represent a valid UTF-8 string
|
||||
*/
|
||||
// The method is tested via test/jdk/java/awt/wakefield/im/text_input_unstable_v3/WaylandPreeditStringToJavaConversionTest.java
|
||||
public static JavaPreeditString fromWaylandPreeditString(
|
||||
final byte[] utf8Bytes,
|
||||
final int cursorBeginUtf8Byte,
|
||||
final int cursorEndUtf8Byte
|
||||
) {
|
||||
// Java's UTF-8 -> UTF-16 conversion doesn't like trailing NUL codepoints, so let's trim them
|
||||
final int utf8BytesWithoutNulLength = Utilities.getLengthOfUtf8BytesWithoutTrailingNULs(utf8Bytes);
|
||||
) throws CharacterCodingException {
|
||||
// The only Unicode code point that can contain zero byte(s) is U+000000.
|
||||
// It hardly makes sense to have it at the end of preedit/commit strings, so let's trim it.
|
||||
final int utf8BytesCorrectedLength = Utilities.getLengthOfUtf8BytesWithoutTrailingNULs(utf8Bytes);
|
||||
|
||||
// cursorBeginUtf8Byte, cursorEndUtf8Byte normalized relatively to the valid values range.
|
||||
final int fixedCursorBeginUtf8Byte;
|
||||
@@ -69,37 +107,116 @@ record JavaPreeditString(String text, int cursorBeginCodeUnit, int cursorEndCode
|
||||
if (cursorBeginUtf8Byte < 0 || cursorEndUtf8Byte < 0) {
|
||||
fixedCursorBeginUtf8Byte = fixedCursorEndUtf8Byte = -1;
|
||||
} else {
|
||||
// 0 <= cursorBeginUtf8Byte <= fixedCursorBeginUtf8Byte <= utf8BytesWithoutNulLength
|
||||
fixedCursorBeginUtf8Byte = Math.min(cursorBeginUtf8Byte, utf8BytesWithoutNulLength);
|
||||
// 0 <= cursorEndUtf8Byte <= fixedCursorEndUtf8Byte <= utf8BytesWithoutNulLength
|
||||
fixedCursorEndUtf8Byte = Math.min(cursorEndUtf8Byte, utf8BytesWithoutNulLength);
|
||||
// 0 <= cursorBeginUtf8Byte <= fixedCursorBeginUtf8Byte <= utf8BytesCorrectedLength
|
||||
fixedCursorBeginUtf8Byte = Math.min(cursorBeginUtf8Byte, utf8BytesCorrectedLength);
|
||||
// 0 <= cursorEndUtf8Byte <= fixedCursorEndUtf8Byte <= utf8BytesCorrectedLength
|
||||
fixedCursorEndUtf8Byte = Math.min(cursorEndUtf8Byte, utf8BytesCorrectedLength);
|
||||
}
|
||||
|
||||
final var resultText = Utilities.utf8BytesToJavaString(utf8Bytes, 0, utf8BytesWithoutNulLength);
|
||||
if (utf8BytesCorrectedLength < 1) {
|
||||
return fixedCursorBeginUtf8Byte < 0 || fixedCursorEndUtf8Byte < 0
|
||||
? JavaPreeditString.EMPTY_NO_CARET
|
||||
: JavaPreeditString.EMPTY;
|
||||
}
|
||||
|
||||
final CharsetDecoder utf8Decoder = StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
// there can't be unmappable characters for this
|
||||
// kind of conversion, so REPLACE is just in case
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
final CharBuffer decodingBuffer = CharBuffer.allocate(utf8BytesCorrectedLength + 1);
|
||||
final ByteBuffer utf8BytesBuffer = ByteBuffer.wrap(utf8Bytes, 0, utf8BytesCorrectedLength);
|
||||
CoderResult decodingResult;
|
||||
|
||||
// The decoding will be performed in 3 sections:
|
||||
// [0; decodingPoint1) + [decodingPoint1; decodingPoint2) + [decodingPoint2; utf8BytesCorrectedLength),
|
||||
// where decodingPoint1, decodingPoint2 are the fixedCursor[Begin|End]Utf8Byte
|
||||
// This way we can translate the fixedCursor[Begin|End]Utf8Byte to their UTF-16 equivalents right while decoding.
|
||||
|
||||
final int decodingPoint1 = Math.min(fixedCursorBeginUtf8Byte, fixedCursorEndUtf8Byte);
|
||||
final int decodingPoint2 = Math.max(fixedCursorBeginUtf8Byte, fixedCursorEndUtf8Byte);
|
||||
|
||||
final int decodedPoint1;
|
||||
final int decodedPoint2;
|
||||
|
||||
// [0; decodingPoint1)
|
||||
if (decodingPoint1 > 0) {
|
||||
utf8BytesBuffer.limit(decodingPoint1);
|
||||
|
||||
decodingResult = utf8Decoder.decode(utf8BytesBuffer, decodingBuffer, false);
|
||||
if (decodingResult.isError() || decodingResult.isOverflow()) {
|
||||
decodingResult.throwException();
|
||||
}
|
||||
|
||||
decodedPoint1 = decodingBuffer.position();
|
||||
} else {
|
||||
decodedPoint1 = decodingPoint1 < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
// [decodingPoint1; decodingPoint2)
|
||||
if (decodingPoint2 > 0 && decodingPoint2 > decodingPoint1) {
|
||||
utf8BytesBuffer.limit(decodingPoint2);
|
||||
|
||||
decodingResult = utf8Decoder.decode(utf8BytesBuffer, decodingBuffer, false);
|
||||
if (decodingResult.isError() || decodingResult.isOverflow()) {
|
||||
decodingResult.throwException();
|
||||
}
|
||||
|
||||
decodedPoint2 = decodingBuffer.position();
|
||||
} else {
|
||||
decodedPoint2 = decodingPoint2 < 0 ? -1 : decodedPoint1;
|
||||
}
|
||||
|
||||
// [decodingPoint2; utf8BytesCorrectedLength)
|
||||
{
|
||||
utf8BytesBuffer.limit(utf8BytesCorrectedLength);
|
||||
|
||||
decodingResult = utf8Decoder.decode(utf8BytesBuffer, decodingBuffer, true);
|
||||
if (decodingResult.isError() || decodingResult.isOverflow()) {
|
||||
decodingResult.throwException();
|
||||
}
|
||||
}
|
||||
|
||||
// last decoding step
|
||||
decodingResult = utf8Decoder.flush(decodingBuffer);
|
||||
if (decodingResult.isError() || decodingResult.isOverflow()) {
|
||||
decodingResult.throwException();
|
||||
}
|
||||
|
||||
// From now on we know that utf8Bytes really represents a properly encoded UTF-8 string.
|
||||
|
||||
// -1 <= decodedPoint1 <= decodedPoint2 <= utf8BytesCorrectedLength
|
||||
assert(decodedPoint1 >= -1);
|
||||
assert(decodedPoint2 >= decodedPoint1);
|
||||
assert(decodedPoint2 <= utf8BytesCorrectedLength);
|
||||
|
||||
final String resultText = decodingBuffer.flip().toString();
|
||||
final int resultCursorBeginCodeUnit;
|
||||
final int resultCursorEndCodeUnit;
|
||||
|
||||
if (fixedCursorBeginUtf8Byte < 0 || fixedCursorEndUtf8Byte < 0) {
|
||||
return new JavaPreeditString(resultText, -1, -1);
|
||||
resultCursorBeginCodeUnit = resultCursorEndCodeUnit = -1;
|
||||
} else {
|
||||
// 0 <= decodedPoint1 <= decodedPoint2 <= utf8BytesCorrectedLength
|
||||
assert(decodedPoint1 >= 0);
|
||||
|
||||
if (Utilities.isUtf8CharBoundary(fixedCursorBeginUtf8Byte, utf8Bytes, utf8BytesCorrectedLength)) {
|
||||
resultCursorBeginCodeUnit = fixedCursorBeginUtf8Byte < fixedCursorEndUtf8Byte
|
||||
? decodedPoint1
|
||||
: decodedPoint2;
|
||||
|
||||
if (Utilities.isUtf8CharBoundary(fixedCursorEndUtf8Byte, utf8Bytes, utf8BytesCorrectedLength)) {
|
||||
resultCursorEndCodeUnit = fixedCursorBeginUtf8Byte < fixedCursorEndUtf8Byte
|
||||
? decodedPoint2
|
||||
: decodedPoint1;
|
||||
} else {
|
||||
resultCursorEndCodeUnit = resultCursorBeginCodeUnit;
|
||||
}
|
||||
} else {
|
||||
resultCursorBeginCodeUnit = resultCursorEndCodeUnit = resultText.length();
|
||||
}
|
||||
}
|
||||
|
||||
if (resultText == null) {
|
||||
assert fixedCursorBeginUtf8Byte == 0 : "Cursor begin byte must be zero for an empty string";
|
||||
assert fixedCursorEndUtf8Byte == 0 : "Cursor end byte must be zero for an empty string";
|
||||
|
||||
return JavaPreeditString.EMPTY;
|
||||
}
|
||||
|
||||
final String javaPrefixBeforeCursorBegin = (fixedCursorBeginUtf8Byte == 0)
|
||||
? ""
|
||||
: Utilities.utf8BytesToJavaString(utf8Bytes, 0, fixedCursorBeginUtf8Byte);
|
||||
|
||||
final String javaPrefixBeforeCursorEnd = (fixedCursorEndUtf8Byte == 0)
|
||||
? ""
|
||||
: Utilities.utf8BytesToJavaString(utf8Bytes, 0, fixedCursorEndUtf8Byte);
|
||||
|
||||
return new JavaPreeditString(
|
||||
resultText,
|
||||
javaPrefixBeforeCursorBegin.length(),
|
||||
javaPrefixBeforeCursorEnd.length()
|
||||
);
|
||||
return new JavaPreeditString(resultText, resultCursorBeginCodeUnit, resultCursorEndCodeUnit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 JetBrains s.r.o.
|
||||
* Copyright 2025-2026 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -25,8 +25,6 @@
|
||||
|
||||
package sun.awt.wl.im.text_input_unstable_v3;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
|
||||
interface Utilities {
|
||||
static int getLengthOfUtf8BytesWithoutTrailingNULs(final byte[] utf8Bytes) {
|
||||
@@ -40,20 +38,34 @@ interface Utilities {
|
||||
return (lastNonNulIndex < 0) ? 0 : lastNonNulIndex + 1;
|
||||
}
|
||||
|
||||
static String utf8BytesToJavaString(final byte[] utf8Bytes) {
|
||||
if (utf8Bytes == null) {
|
||||
return "";
|
||||
/**
|
||||
* Checks that {@code index}-th byte is the first byte in a UTF-8 code point sequence or the end of the string.
|
||||
*
|
||||
* @param index index of the byte in {@code utf8StrBytes} to check
|
||||
* @param utf8StrBytes byte array representing a correctly encoded UTF-8 string
|
||||
* @param utf8StrBytesLength if non-negative, will be considered as the array length instead of {@code utf8StrBytes.length}
|
||||
* @return {@code true} if either {@code index} points to the end of the string or to a first byte of a code point
|
||||
*/
|
||||
static boolean isUtf8CharBoundary(final int index, final byte[] utf8StrBytes, int utf8StrBytesLength) {
|
||||
utf8StrBytesLength = utf8StrBytesLength < 0 ? utf8StrBytes.length : utf8StrBytesLength;
|
||||
|
||||
if (utf8StrBytesLength > utf8StrBytes.length) {
|
||||
throw new ArrayIndexOutOfBoundsException("utf8StrBytesLength");
|
||||
}
|
||||
if (index < 0 || index > utf8StrBytesLength) {
|
||||
throw new ArrayIndexOutOfBoundsException("index");
|
||||
}
|
||||
|
||||
return utf8BytesToJavaString(
|
||||
utf8Bytes,
|
||||
0,
|
||||
// Java's UTF-8 -> UTF-16 conversion doesn't like trailing NUL codepoints, so let's trim them
|
||||
getLengthOfUtf8BytesWithoutTrailingNULs(utf8Bytes)
|
||||
);
|
||||
}
|
||||
if (index == utf8StrBytesLength) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static String utf8BytesToJavaString(final byte[] utf8Bytes, final int offset, final int length) {
|
||||
return utf8Bytes == null ? "" : new String(utf8Bytes, offset, length, StandardCharsets.UTF_8);
|
||||
final byte utf8Byte = utf8StrBytes[index];
|
||||
|
||||
// In a valid UTF-8 string, a byte is the first byte of an encoded code point if and only if
|
||||
// its binary representation does NOT start with 10, i.e. does NOT match 0b10......
|
||||
|
||||
final int utf8ByteUnsigned = Byte.toUnsignedInt(utf8Byte);
|
||||
return ((utf8ByteUnsigned & 0b1100_0000) != 0b1000_0000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 JetBrains s.r.o.
|
||||
* Copyright 2025-2026 JetBrains s.r.o.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -659,18 +659,12 @@ final class WLInputMethodZwpTextInputV3 extends InputMethodAdapter {
|
||||
final int highlightEndCodeUnitIndex =
|
||||
Math.max(0, Math.min(preeditString.cursorEndCodeUnit(), preeditString.text().length()));
|
||||
|
||||
// Mutter doesn't seem to send preedit_string events with highlighting
|
||||
// (i.e. they never have cursor_begin != cursor_end) at all.
|
||||
// KWin, however, always uses highlighting. Looking at how it changes when we navigate within the
|
||||
// preedit text with arrow keys, it becomes clear KWin expects the caret to be put at the end
|
||||
// of the highlighting and not at the beginning.
|
||||
// That's why highlightEndCodeUnitIndex is used here and not highlightBeginCodeUnitIndex.
|
||||
imeCaret = TextHitInfo.beforeOffset(highlightEndCodeUnitIndex);
|
||||
imeCaret = TextHitInfo.beforeOffset(highlightBeginCodeUnitIndex);
|
||||
|
||||
// cursor_begin and cursor_end
|
||||
// "could be represented by the client as a line if both values are the same,
|
||||
// or as a text highlight otherwise"
|
||||
if (highlightEndCodeUnitIndex == highlightBeginCodeUnitIndex) {
|
||||
if (highlightBeginCodeUnitIndex == highlightEndCodeUnitIndex) {
|
||||
// Only basic highlighting
|
||||
awtInstallIMHighlightingInto(imeText, commitString.text().length(), preeditString.text().length(), 0, 0);
|
||||
} else {
|
||||
@@ -745,8 +739,6 @@ final class WLInputMethodZwpTextInputV3 extends InputMethodAdapter {
|
||||
// (e.g. via environment variables).
|
||||
// For now we're adjusting to iBus, because it seems to be the most widespread engine.
|
||||
final InputMethodHighlight IM_BASIC_HIGHLIGHTING = InputMethodHighlight.UNSELECTED_CONVERTED_TEXT_HIGHLIGHT;
|
||||
// I'm not sure if the "text highlight" mentioned in zwp_text_input_v3::preedit_string means the text
|
||||
// should look selected.
|
||||
final InputMethodHighlight IM_SPECIAL_HIGHLIGHTING = InputMethodHighlight.SELECTED_CONVERTED_TEXT_HIGHLIGHT;
|
||||
|
||||
if (specialPreeditHighlightingBegin == specialPreeditHighlightingEnd) {
|
||||
@@ -1204,6 +1196,29 @@ final class WLInputMethodZwpTextInputV3 extends InputMethodAdapter {
|
||||
return wlIncomingChanges;
|
||||
}
|
||||
|
||||
|
||||
private JavaPreeditString wlFixPreeditStringIfBroken(final JavaPreeditString preeditString) {
|
||||
if (preeditString == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final boolean isGNOME46OrBelow;
|
||||
if (CurrentDesktopInfo.isGnome()) {
|
||||
final int gnomeVersion = CurrentDesktopInfo.getGnomeShellMajorVersion();
|
||||
isGNOME46OrBelow = gnomeVersion >= 0 && gnomeVersion <= 46;
|
||||
} else {
|
||||
isGNOME46OrBelow = false;
|
||||
}
|
||||
|
||||
// https://gitlab.gnome.org/GNOME/mutter/-/issues/3547.
|
||||
// Working around it here by resetting cursor_end to cursor_begin.
|
||||
if (isGNOME46OrBelow) {
|
||||
return new JavaPreeditString(preeditString.text(), preeditString.cursorBeginCodeUnit(), preeditString.cursorBeginCodeUnit());
|
||||
}
|
||||
return preeditString;
|
||||
}
|
||||
|
||||
|
||||
/** Called by {@link ClientComponentCaretPositionTracker} */
|
||||
boolean wlUpdateCursorRectangle(final boolean forceUpdate) {
|
||||
assert EventQueue.isDispatchThread() : "Method must only be invoked on EDT";
|
||||
@@ -1320,9 +1335,9 @@ final class WLInputMethodZwpTextInputV3 extends InputMethodAdapter {
|
||||
}
|
||||
|
||||
if (wlInputContextState.getCurrentWlSurfacePtr() != leftWlSurfacePtr) {
|
||||
if (log.isLoggable(PlatformLogger.Level.WARNING)) {
|
||||
log.warning("zwp_text_input_v3_onLeave: leftWlSurfacePtr==0x{0} isn''t equal to the currently known one 0x{1}.",
|
||||
Long.toHexString(leftWlSurfacePtr), Long.toHexString(wlInputContextState.getCurrentWlSurfacePtr()));
|
||||
if (log.isLoggable(PlatformLogger.Level.INFO)) {
|
||||
log.info("zwp_text_input_v3_onLeave: leftWlSurfacePtr==0x{0} isn''t equal to the currently known one 0x{1}.",
|
||||
Long.toHexString(leftWlSurfacePtr), Long.toHexString(wlInputContextState.getCurrentWlSurfacePtr()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1423,8 +1438,34 @@ final class WLInputMethodZwpTextInputV3 extends InputMethodAdapter {
|
||||
preeditStringToApply = PropertiesInitials.PREEDIT_STRING;
|
||||
commitStringToApply = PropertiesInitials.COMMIT_STRING;
|
||||
} else {
|
||||
preeditStringToApply = Objects.requireNonNullElse(incomingChangesToApply.getPreeditString(), PropertiesInitials.PREEDIT_STRING);
|
||||
commitStringToApply = Objects.requireNonNullElse(incomingChangesToApply.getCommitString(), PropertiesInitials.COMMIT_STRING);
|
||||
JavaPreeditString preeditStringToApplyInitializer;
|
||||
try {
|
||||
preeditStringToApplyInitializer = incomingChangesToApply.getPreeditString();
|
||||
} catch (IncomingChanges.ConversionException err) {
|
||||
preeditStringToApplyInitializer = JavaPreeditString.EMPTY;
|
||||
if (log.isLoggable(PlatformLogger.Level.WARNING)) {
|
||||
log.warning(
|
||||
String.format("Failed to obtain the preedit string from the incoming changes, instead will use %s.", preeditStringToApplyInitializer),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
preeditStringToApplyInitializer = wlFixPreeditStringIfBroken(preeditStringToApplyInitializer);
|
||||
preeditStringToApply = Objects.requireNonNullElse(preeditStringToApplyInitializer, PropertiesInitials.PREEDIT_STRING);
|
||||
|
||||
JavaCommitString commitStringToApplyInitializer;
|
||||
try {
|
||||
commitStringToApplyInitializer = incomingChangesToApply.getCommitString();
|
||||
} catch (IncomingChanges.ConversionException err) {
|
||||
commitStringToApplyInitializer = JavaCommitString.EMPTY;
|
||||
if (log.isLoggable(PlatformLogger.Level.WARNING)) {
|
||||
log.warning(
|
||||
String.format("Failed to obtain the commit string from the incoming changes, instead will use %s.", commitStringToApplyInitializer),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
commitStringToApply = Objects.requireNonNullElse(commitStringToApplyInitializer, PropertiesInitials.COMMIT_STRING);
|
||||
}
|
||||
|
||||
this.wlInputContextState.syncWithAppliedIncomingChanges(preeditStringToApply, commitStringToApply, doneSerial);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.SimpleEmptyImpl com.jetbrains.test.api.MethodMapping.SimpleEmpty PROVIDES
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.PlainImpl com.jetbrains.test.api.MethodMapping.Plain SERVICE
|
||||
PROVIDES com.jetbrains.test.jbr.MethodMapping.SimpleEmptyImpl com.jetbrains.test.api.MethodMapping.SimpleEmpty
|
||||
SERVICE com.jetbrains.test.jbr.MethodMapping.PlainImpl com.jetbrains.test.api.MethodMapping.Plain
|
||||
STATIC MethodMappingTest main ([Ljava/lang/String;)V com.jetbrains.test.api.MethodMapping.Plain c
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.PlainFailImpl com.jetbrains.test.api.MethodMapping.PlainFail SERVICE
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.Callback com.jetbrains.test.api.MethodMapping.ApiCallback PROVIDED
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.JBRTwoWay com.jetbrains.test.api.MethodMapping.ApiTwoWay TWO_WAY
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.ConversionImpl com.jetbrains.test.api.MethodMapping.Conversion TWO_WAY
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.ConversionSelfImpl com.jetbrains.test.api.MethodMapping.ConversionSelf PROVIDES
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.ConversionFailImpl com.jetbrains.test.api.MethodMapping.ConversionFail PROVIDES
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.ArrayConversionImpl com.jetbrains.test.api.MethodMapping.ArrayConversion PROVIDES
|
||||
TYPE com.jetbrains.test.jbr.MethodMapping.GenericConversionImpl com.jetbrains.test.api.MethodMapping.GenericConversion PROVIDES
|
||||
SERVICE com.jetbrains.test.jbr.MethodMapping.PlainFailImpl com.jetbrains.test.api.MethodMapping.PlainFail
|
||||
PROVIDED com.jetbrains.test.jbr.MethodMapping.Callback com.jetbrains.test.api.MethodMapping.ApiCallback
|
||||
TWO_WAY com.jetbrains.test.jbr.MethodMapping.JBRTwoWay com.jetbrains.test.api.MethodMapping.ApiTwoWay
|
||||
TWO_WAY com.jetbrains.test.jbr.MethodMapping.ConversionImpl com.jetbrains.test.api.MethodMapping.Conversion
|
||||
PROVIDES com.jetbrains.test.jbr.MethodMapping.ConversionSelfImpl com.jetbrains.test.api.MethodMapping.ConversionSelf
|
||||
PROVIDES com.jetbrains.test.jbr.MethodMapping.ConversionFailImpl com.jetbrains.test.api.MethodMapping.ConversionFail
|
||||
PROVIDES com.jetbrains.test.jbr.MethodMapping.ArrayConversionImpl com.jetbrains.test.api.MethodMapping.ArrayConversion
|
||||
PROVIDES com.jetbrains.test.jbr.MethodMapping.GenericConversionImpl com.jetbrains.test.api.MethodMapping.GenericConversion
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
TYPE absentImpl com.jetbrains.test.api.ProxyInfoResolving.InterfaceWithoutImplementation PROVIDES
|
||||
PROVIDES absentImpl com.jetbrains.test.api.ProxyInfoResolving.InterfaceWithoutImplementation
|
||||
STATIC NoClass foo ()V com.jetbrains.test.api.ProxyInfoResolving.ServiceWithoutImplementation foo
|
||||
TYPE com.jetbrains.test.jbr.ProxyInfoResolving.ValidApiImpl com.jetbrains.test.api.ProxyInfoResolving.ValidApi PROVIDES
|
||||
TYPE com.jetbrains.test.jbr.ProxyInfoResolving.ProxyClassImpl com.jetbrains.test.api.ProxyInfoResolving.ProxyClass PROVIDES
|
||||
TYPE com.jetbrains.test.jbr.ProxyInfoResolving.ClientProxyClass com.jetbrains.test.api.ProxyInfoResolving.ClientProxyClassImpl PROVIDED
|
||||
TYPE com.jetbrains.test.jbr.ProxyInfoResolving.ServiceWithoutAnnotationImpl com.jetbrains.test.api.ProxyInfoResolving.ServiceWithoutAnnotation SERVICE
|
||||
PROVIDES com.jetbrains.test.jbr.ProxyInfoResolving.ValidApiImpl com.jetbrains.test.api.ProxyInfoResolving.ValidApi
|
||||
PROVIDES com.jetbrains.test.jbr.ProxyInfoResolving.ProxyClassImpl com.jetbrains.test.api.ProxyInfoResolving.ProxyClass
|
||||
PROVIDED com.jetbrains.test.jbr.ProxyInfoResolving.ClientProxyClass com.jetbrains.test.api.ProxyInfoResolving.ClientProxyClassImpl
|
||||
SERVICE com.jetbrains.test.jbr.ProxyInfoResolving.ServiceWithoutAnnotationImpl com.jetbrains.test.api.ProxyInfoResolving.ServiceWithoutAnnotation
|
||||
STATIC NoClass foo ()V com.jetbrains.test.api.ProxyInfoResolving.ServiceWithExtension foo
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
import com.jetbrains.Extensions;
|
||||
import com.jetbrains.internal.jbrapi.JBRApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -41,11 +43,25 @@ import static com.jetbrains.test.api.Real.*;
|
||||
|
||||
public class RealTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
init("RealTest", Map.of(Extensions.FOO, new Class[] {Proxy.class}, Extensions.BAR, new Class[] {Proxy.class}));
|
||||
public static void main(String[] args) throws Throwable {
|
||||
// Plain run.
|
||||
run();
|
||||
|
||||
// Run in an isolated classloader at least 2 times until the proxy gets GC'ed.
|
||||
WeakReference<?> weak = null;
|
||||
for (int i = 0; i < 2 || weak.get() != null; i++) {
|
||||
System.gc();
|
||||
new IsolatedLoader().loadClass(RealTest.class.getName()).getMethod("run").invoke(null);
|
||||
if (weak == null) weak = new WeakReference<>(getProxy(Proxy.class));
|
||||
if (i > 300) throw new Error("Proxy was not collected after 300 iterations");
|
||||
}
|
||||
}
|
||||
|
||||
public static void run() {
|
||||
init("RealTest", Map.of(Extensions.FOO, new Class<?>[] {Proxy.class}, Extensions.BAR, new Class<?>[] {Proxy.class}));
|
||||
|
||||
// Get service
|
||||
Service service = Objects.requireNonNull(JBRApi.getService(Service.class));
|
||||
Service service = Objects.requireNonNull(api.getService(Service.class));
|
||||
|
||||
// Proxy passthrough
|
||||
Proxy p = Objects.requireNonNull(service.getProxy());
|
||||
@@ -86,10 +102,10 @@ public class RealTest {
|
||||
}
|
||||
|
||||
// Check extensions
|
||||
if (!JBRApi.isExtensionSupported(Extensions.FOO)) {
|
||||
if (!api.isExtensionSupported(Extensions.FOO)) {
|
||||
throw new Error("FOO extension must be supported");
|
||||
}
|
||||
if (JBRApi.isExtensionSupported(Extensions.BAR)) {
|
||||
if (api.isExtensionSupported(Extensions.BAR)) {
|
||||
throw new Error("BAR extension must not be supported");
|
||||
}
|
||||
try {
|
||||
@@ -101,10 +117,10 @@ public class RealTest {
|
||||
throw new Error("BAR extension was disabled but call succeeded");
|
||||
} catch (UnsupportedOperationException ignore) {}
|
||||
// foo() must succeed when enabled
|
||||
JBRApi.getService(Service.class, Extensions.FOO).getProxy().foo();
|
||||
api.getService(Service.class, Extensions.FOO).getProxy().foo();
|
||||
// Asking for BAR must return null, as it is not supported
|
||||
requireNull(JBRApi.getService(Service.class, Extensions.FOO, Extensions.BAR));
|
||||
requireNull(JBRApi.getService(Service.class, Extensions.BAR));
|
||||
requireNull(api.getService(Service.class, Extensions.FOO, Extensions.BAR));
|
||||
requireNull(api.getService(Service.class, Extensions.BAR));
|
||||
|
||||
// Test specialized (implicit) List proxy
|
||||
List<Api2Way> list = Objects.requireNonNull(service.testList(null));
|
||||
@@ -133,4 +149,25 @@ public class RealTest {
|
||||
value = o;
|
||||
}
|
||||
}
|
||||
|
||||
private static class IsolatedLoader extends ClassLoader {
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (name.equals("RealClassloadersTest") ||
|
||||
(name.startsWith("com.jetbrains.") && !name.startsWith("com.jetbrains.test.jbr.") &&
|
||||
!name.startsWith("com.jetbrains.exported.") && !name.startsWith("com.jetbrains.internal."))) {
|
||||
Class<?> c = findLoadedClass(name);
|
||||
if (c != null) return c;
|
||||
String path = name.replace('.', '/').concat(".class");
|
||||
try (var stream = getResourceAsStream(path)) {
|
||||
if (stream == null) throw new ClassNotFoundException(name);
|
||||
byte[] b = stream.readAllBytes();
|
||||
return defineClass(name, b, 0, b.length);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
TYPE com.jetbrains.test.jbr.Real.ServiceImpl com.jetbrains.test.api.Real.Service SERVICE
|
||||
TYPE com.jetbrains.test.jbr.Real.ProxyImpl com.jetbrains.test.api.Real.Proxy PROVIDES
|
||||
TYPE com.jetbrains.test.jbr.Real.Client com.jetbrains.test.api.Real.ClientImpl PROVIDED
|
||||
TYPE com.jetbrains.test.jbr.Real.JBR2Way com.jetbrains.test.api.Real.Api2Way TWO_WAY
|
||||
TYPE com.jetbrains.test.jbr.Real.JBRLazyNumber com.jetbrains.test.api.Real.ApiLazyNumber TWO_WAY
|
||||
SERVICE com.jetbrains.test.jbr.Real.ServiceImpl com.jetbrains.test.api.Real.Service
|
||||
PROVIDES com.jetbrains.test.jbr.Real.ProxyImpl com.jetbrains.test.api.Real.Proxy
|
||||
PROVIDED com.jetbrains.test.jbr.Real.Client com.jetbrains.test.api.Real.ClientImpl
|
||||
TWO_WAY com.jetbrains.test.jbr.Real.JBR2Way com.jetbrains.test.api.Real.Api2Way
|
||||
TWO_WAY com.jetbrains.test.jbr.Real.JBRLazyNumber com.jetbrains.test.api.Real.ApiLazyNumber
|
||||
|
||||
@@ -28,5 +28,7 @@ package com.jetbrains;
|
||||
*/
|
||||
public class JBR {
|
||||
|
||||
@Service
|
||||
@Provided
|
||||
public interface ServiceApi {}
|
||||
}
|
||||
|
||||
@@ -32,31 +32,40 @@ import java.util.Map;
|
||||
|
||||
public class Util {
|
||||
|
||||
public static JBRApi api;
|
||||
private static Object proxyRepository;
|
||||
private static Method getProxy, inverse, generate;
|
||||
|
||||
/**
|
||||
* Invoke internal {@link JBRApi#init} bypassing {@link com.jetbrains.exported.JBRApiSupport#bootstrap}.
|
||||
*/
|
||||
public static void init(String registryName, Map<Enum<?>, Class[]> extensionClasses) {
|
||||
try (InputStream in = new FileInputStream(new File(System.getProperty("test.src", "."), registryName + ".registry"))) {
|
||||
JBRApi.init(in, Service.class, Provided.class, Provides.class, extensionClasses, m -> {
|
||||
public static void init(String registryName, Map<Enum<?>, Class<?>[]> extensionClasses) {
|
||||
try (InputStream in = new SequenceInputStream(
|
||||
new ByteArrayInputStream("PROVIDES com.jetbrains.internal.jbrapi.JBRApi com.jetbrains.JBR.ServiceApi\n".getBytes()),
|
||||
new FileInputStream(new File(System.getProperty("test.src", "."), registryName + ".registry")))) {
|
||||
Object api = JBRApi.init(in, JBR.ServiceApi.class, Service.class, Provided.class, Provides.class, extensionClasses, m -> {
|
||||
Extension e = m.getAnnotation(Extension.class);
|
||||
return e == null ? null : e.value();
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Field f = api.getClass().getDeclaredField("target");
|
||||
f.setAccessible(true);
|
||||
Util.api = (JBRApi) f.get(api);
|
||||
proxyRepository = null;
|
||||
getProxy = inverse = generate = null;
|
||||
} catch (IOException | NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Object proxyRepository;
|
||||
public static Object getProxyRepository() throws Throwable {
|
||||
if (proxyRepository == null) {
|
||||
Field f = JBRApi.class.getDeclaredField("proxyRepository");
|
||||
f.setAccessible(true);
|
||||
proxyRepository = f.get(null);
|
||||
proxyRepository = f.get(api);
|
||||
}
|
||||
return proxyRepository;
|
||||
}
|
||||
|
||||
private static Method getProxy;
|
||||
public static Object getProxy(Class<?> interFace) throws Throwable {
|
||||
var repo = getProxyRepository();
|
||||
if (getProxy == null) {
|
||||
@@ -67,7 +76,6 @@ public class Util {
|
||||
return getProxy.invoke(repo, interFace, null);
|
||||
}
|
||||
|
||||
private static Method inverse;
|
||||
public static Object inverse(Object proxy) throws Throwable {
|
||||
if (inverse == null) {
|
||||
inverse = proxy.getClass().getDeclaredMethod("inverse");
|
||||
@@ -76,7 +84,6 @@ public class Util {
|
||||
return inverse.invoke(proxy);
|
||||
}
|
||||
|
||||
private static Method generate;
|
||||
public static boolean isSupported(Object proxy) throws Throwable {
|
||||
if (generate == null) {
|
||||
generate = proxy.getClass().getDeclaredMethod("generate");
|
||||
|
||||
Reference in New Issue
Block a user