mirror of
https://github.com/JetBrains/JetBrainsRuntime.git
synced 2026-01-31 20:56:28 +01:00
Compare commits
310 Commits
jbr25
...
vpr/update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e4b9ab9fe | ||
|
|
118915d36a | ||
|
|
d2f6f04b1c | ||
|
|
639ad76841 | ||
|
|
70501a7163 | ||
|
|
530538ec6e | ||
|
|
3195df109f | ||
|
|
26e637f5e8 | ||
|
|
8de23b6208 | ||
|
|
d360ca79ed | ||
|
|
8d3bfa4fe0 | ||
|
|
9018865725 | ||
|
|
bcddf0fa77 | ||
|
|
c3ab7dfff8 | ||
|
|
7c50ced913 | ||
|
|
4bbbc89bff | ||
|
|
94203b85f9 | ||
|
|
2ea60ba454 | ||
|
|
3526c6957b | ||
|
|
7f47b088d8 | ||
|
|
7e424d49f6 | ||
|
|
f18d1fd2dd | ||
|
|
7d8cc48e1e | ||
|
|
fd7a9c35f3 | ||
|
|
17e0cbad03 | ||
|
|
1dd7512bbd | ||
|
|
d3e8ef53b7 | ||
|
|
f1b6a6b4cd | ||
|
|
31757a29da | ||
|
|
aece9e0b48 | ||
|
|
a2d2229ff4 | ||
|
|
d2050cecaf | ||
|
|
d2ffd294d8 | ||
|
|
e3e335e449 | ||
|
|
1b8418aad8 | ||
|
|
84c7e312b2 | ||
|
|
754e25e8e9 | ||
|
|
f5375d45d2 | ||
|
|
975659cbac | ||
|
|
561bca6b25 | ||
|
|
be88154727 | ||
|
|
d961fae855 | ||
|
|
15e7bc5997 | ||
|
|
88809673ff | ||
|
|
cd78fb00e3 | ||
|
|
6f55e6f965 | ||
|
|
4f53417379 | ||
|
|
f36e7808f5 | ||
|
|
021bdd427f | ||
|
|
c47029b756 | ||
|
|
109f16f7cd | ||
|
|
184a465e1b | ||
|
|
d158ba0a3d | ||
|
|
fe1aafd0e6 | ||
|
|
3f35cb9022 | ||
|
|
5157aef64d | ||
|
|
13176e057f | ||
|
|
a8deb6c6dd | ||
|
|
a1d77270b4 | ||
|
|
7b486e585a | ||
|
|
64fd28006a | ||
|
|
22334869b7 | ||
|
|
5a7a4824b4 | ||
|
|
d3fede2b54 | ||
|
|
641b2ca788 | ||
|
|
20cc191e4a | ||
|
|
89acfe5f73 | ||
|
|
80bc539570 | ||
|
|
5f302fa47a | ||
|
|
c8b6f5e737 | ||
|
|
2ce6d1b10f | ||
|
|
92a00c7161 | ||
|
|
85a8c54493 | ||
|
|
405d9bf3fe | ||
|
|
2ea868167c | ||
|
|
98a9b5b028 | ||
|
|
7aa681670e | ||
|
|
897257797e | ||
|
|
624df2cbc8 | ||
|
|
82ef604109 | ||
|
|
015a810254 | ||
|
|
b2d0dbc83c | ||
|
|
34aeacd74b | ||
|
|
58f3c6cd4b | ||
|
|
27d64be387 | ||
|
|
7f04038170 | ||
|
|
4c74718a5a | ||
|
|
3f3d589d0a | ||
|
|
6cf5dd0a35 | ||
|
|
c350a93050 | ||
|
|
d790b11b71 | ||
|
|
04607d8c64 | ||
|
|
2d8093d97b | ||
|
|
bc12ae254d | ||
|
|
6548a94119 | ||
|
|
cae0639937 | ||
|
|
eb39f11e23 | ||
|
|
2e905e946c | ||
|
|
56a04bf4bb | ||
|
|
1189a34e5c | ||
|
|
bf66cdd89d | ||
|
|
2632b7cd0c | ||
|
|
97755b0b5f | ||
|
|
810a167ecf | ||
|
|
cfae44b0d2 | ||
|
|
f7574d5ab7 | ||
|
|
3052ccbddd | ||
|
|
70755c1758 | ||
|
|
ccd676d3b6 | ||
|
|
48c47a83cf | ||
|
|
fd7291ddeb | ||
|
|
c16b63da53 | ||
|
|
73ca6fb2e7 | ||
|
|
81e893dac6 | ||
|
|
d7e6471e53 | ||
|
|
f87b4b039e | ||
|
|
0d0a5570ae | ||
|
|
95574775a7 | ||
|
|
1589cd5c5b | ||
|
|
60a0d6fa91 | ||
|
|
15902d647b | ||
|
|
0e47243065 | ||
|
|
d587e632e4 | ||
|
|
cca28e69ac | ||
|
|
f14e1ff84d | ||
|
|
8dbe5153ed | ||
|
|
9bfaad73a1 | ||
|
|
e2c04903a9 | ||
|
|
6abf323fa3 | ||
|
|
26f7077a1b | ||
|
|
11222fd787 | ||
|
|
463f5d609c | ||
|
|
aa31216d20 | ||
|
|
86010cca73 | ||
|
|
3da1b7eec8 | ||
|
|
c6c56bc5f5 | ||
|
|
83ed2a919c | ||
|
|
49f16101c9 | ||
|
|
da9779c58b | ||
|
|
e05391200f | ||
|
|
a0e9c79747 | ||
|
|
43d9ce5f08 | ||
|
|
5062dfe21e | ||
|
|
04294c19d9 | ||
|
|
43a2a0e4f5 | ||
|
|
3cba7bcf3f | ||
|
|
4a3b5b5ee1 | ||
|
|
77596c3ebf | ||
|
|
01fdc6ebbb | ||
|
|
864f6ff97b | ||
|
|
088bb7fce4 | ||
|
|
12b8fb0900 | ||
|
|
ca855d5e63 | ||
|
|
3a2e400923 | ||
|
|
56bf0e115b | ||
|
|
cb5df22159 | ||
|
|
60013f13de | ||
|
|
5377bf7b7e | ||
|
|
a7c40d1bb8 | ||
|
|
dc9524f67e | ||
|
|
c352cdf0eb | ||
|
|
4fb763f278 | ||
|
|
10c5585183 | ||
|
|
ac27e8a8e5 | ||
|
|
d0397590ab | ||
|
|
162dab8f17 | ||
|
|
1dfa6ecd61 | ||
|
|
c4f0168cf1 | ||
|
|
12acfeadcb | ||
|
|
646ae0e7da | ||
|
|
bf6d863e7e | ||
|
|
9386c1986a | ||
|
|
87b297a01c | ||
|
|
7efa4a27eb | ||
|
|
1fc59ee493 | ||
|
|
c968a428d2 | ||
|
|
1b845c4470 | ||
|
|
5d40abfa81 | ||
|
|
3a60f5fcda | ||
|
|
fe961b1a90 | ||
|
|
467459c562 | ||
|
|
d6a8b8b8b4 | ||
|
|
5e0e381b1d | ||
|
|
eb3b30eae7 | ||
|
|
98a4962a6a | ||
|
|
0e6ae6d5f6 | ||
|
|
ae7b9224a8 | ||
|
|
00e21e4fad | ||
|
|
cbaefac99d | ||
|
|
1d9115a49d | ||
|
|
47534b7782 | ||
|
|
d2dd010fec | ||
|
|
63c33d115e | ||
|
|
bb23245ab2 | ||
|
|
d16e066a1d | ||
|
|
13aaf6fcf1 | ||
|
|
41f3f7997c | ||
|
|
18d18c3e93 | ||
|
|
ff21db7b43 | ||
|
|
e3933114c2 | ||
|
|
2072cf5d23 | ||
|
|
0f50f0a825 | ||
|
|
78f56e11d2 | ||
|
|
a2176cda2a | ||
|
|
dbfd4cb223 | ||
|
|
a7763d752a | ||
|
|
f5a772cbe5 | ||
|
|
7df4fe508e | ||
|
|
7b7d8ab2c5 | ||
|
|
089609db6c | ||
|
|
b9639251fd | ||
|
|
9c7d58cfa4 | ||
|
|
b3d0b2e798 | ||
|
|
714dd7e73d | ||
|
|
8608882e0b | ||
|
|
490cf2a6ff | ||
|
|
8c29fcd4c5 | ||
|
|
65bddecd80 | ||
|
|
8056da45c4 | ||
|
|
83347b9bfd | ||
|
|
bf075b71f0 | ||
|
|
b47d25dea6 | ||
|
|
c3f246c4ef | ||
|
|
5a343a13a5 | ||
|
|
d9fd896973 | ||
|
|
4cc3992f89 | ||
|
|
256e0ed085 | ||
|
|
eb9cdd0735 | ||
|
|
a5e8b4583f | ||
|
|
a0268397e3 | ||
|
|
c1371f36d8 | ||
|
|
3a3f17b766 | ||
|
|
4e6528ad68 | ||
|
|
5ac9d09b08 | ||
|
|
5dbcc3b386 | ||
|
|
0b21225ae3 | ||
|
|
ffca1c86b2 | ||
|
|
2f5839bf8d | ||
|
|
5e8befaa6d | ||
|
|
6ae876e530 | ||
|
|
fbd9ec1fd9 | ||
|
|
9125902914 | ||
|
|
f55b4c20c4 | ||
|
|
dfa8e909fa | ||
|
|
a26d949ebe | ||
|
|
5d728a613c | ||
|
|
c8c020cfbe | ||
|
|
90e55b8948 | ||
|
|
15179063c5 | ||
|
|
25ae692796 | ||
|
|
ab59a8099a | ||
|
|
8d63a1e21f | ||
|
|
6902cba835 | ||
|
|
6c6eb0bf03 | ||
|
|
20618e5b9b | ||
|
|
ad7e6864a1 | ||
|
|
852b59630c | ||
|
|
bcb661fa9e | ||
|
|
33ba9f2b4e | ||
|
|
8e227418b7 | ||
|
|
2a7dde1c86 | ||
|
|
b59463ed08 | ||
|
|
598af2f3f6 | ||
|
|
fa047c1df5 | ||
|
|
c71ed6754c | ||
|
|
abc49874d5 | ||
|
|
d3c0fca7e2 | ||
|
|
5ee10c7b5e | ||
|
|
7219483217 | ||
|
|
dde2574c26 | ||
|
|
46aa5e4547 | ||
|
|
f0ccd4b67c | ||
|
|
77f95a523c | ||
|
|
0f619d8d87 | ||
|
|
e928b6d966 | ||
|
|
c20b2f2c7a | ||
|
|
36c573e053 | ||
|
|
cf3b9fc954 | ||
|
|
9103091dcd | ||
|
|
be75986980 | ||
|
|
26e1e27631 | ||
|
|
3f87607443 | ||
|
|
9a16d275a3 | ||
|
|
bcd7d72131 | ||
|
|
34ec576d71 | ||
|
|
b182c7e5ba | ||
|
|
2ea239d7d0 | ||
|
|
88406911c3 | ||
|
|
4af9871759 | ||
|
|
e6d3da71e0 | ||
|
|
704361ab8c | ||
|
|
4da38f76f4 | ||
|
|
b47b9d2838 | ||
|
|
921854a72c | ||
|
|
9dadec6d1a | ||
|
|
2a9cf17245 | ||
|
|
3272f2f7c4 | ||
|
|
f5dd1f773b | ||
|
|
f880f1eab2 | ||
|
|
6f3ba3716b | ||
|
|
98b25a39bb | ||
|
|
0fe8ef08ff | ||
|
|
051a468007 | ||
|
|
3b119a1e0c | ||
|
|
7c7127bf8c | ||
|
|
5515295b49 | ||
|
|
debacd66d2 | ||
|
|
4288251be9 | ||
|
|
9b5be66a0c | ||
|
|
3c6f850c51 |
@@ -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) $$(call FixPath, $(TOPDIR)/jb/jbr-api.version)"
|
||||
$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_EXTRA_DEPS := $$($1_EXTRA_DEPS) $$(BUILDTOOLS_OUTPUTDIR)/plugins/_the.COMPILE_JBR_API_PLUGIN_batch
|
||||
endif
|
||||
|
||||
|
||||
@@ -198,35 +198,34 @@ public class JBRApiPlugin implements Plugin {
|
||||
return unresolvedErrors;
|
||||
}
|
||||
|
||||
void read(RandomAccessFile file, boolean internal) throws IOException {
|
||||
void read(RandomAccessFile file) throws IOException {
|
||||
String s;
|
||||
while ((s = file.readLine()) != null) {
|
||||
String[] tokens = s.split(" ");
|
||||
switch (tokens[0]) {
|
||||
case "VERSION" -> {}
|
||||
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 "STATIC" -> {
|
||||
StaticDescriptor descriptor = new StaticDescriptor(new StaticMethod(
|
||||
tokens[1], tokens[2]), tokens[3]);
|
||||
methods.put(descriptor, new StaticMethod(tokens[4], tokens[5]));
|
||||
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]);
|
||||
if (tokens.length > 6 && tokens[6].equals("INTERNAL")) internal.add(descriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void write(RandomAccessFile pub, RandomAccessFile priv) throws IOException {
|
||||
void write(RandomAccessFile file) throws IOException {
|
||||
for (var t : types.entrySet()) {
|
||||
(internal.contains(t.getKey()) ? priv : pub).writeBytes(
|
||||
t.getValue().binding + " " + t.getKey() + " " + t.getValue().type + "\n");
|
||||
file.writeBytes("TYPE " + t.getKey() + " " + t.getValue().type + " " + t.getValue().binding +
|
||||
(internal.contains(t.getKey()) ? " INTERNAL\n" : "\n"));
|
||||
}
|
||||
for (var t : methods.entrySet()) {
|
||||
(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");
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,7 +399,7 @@ public class JBRApiPlugin implements Plugin {
|
||||
|
||||
@Override
|
||||
public void init(JavacTask jt, String... args) {
|
||||
Path pubPath = Path.of(args[0] + ".public"), privPath = Path.of(args[0] + ".private");
|
||||
Path output = Path.of(args[0]);
|
||||
String implVersion;
|
||||
try {
|
||||
implVersion = Files.readString(Path.of(args[1])).strip();
|
||||
@@ -427,21 +426,18 @@ public class JBRApiPlugin implements Plugin {
|
||||
}
|
||||
}.scan(te.getTypeElement(), te.getCompilationUnit());
|
||||
} else if (te.getKind() == TaskEvent.Kind.COMPILATION) {
|
||||
try (RandomAccessFile pub = new RandomAccessFile(pubPath.toFile(), "rw");
|
||||
RandomAccessFile priv = new RandomAccessFile(privPath.toFile(), "rw");
|
||||
FileChannel channel = pub.getChannel()) {
|
||||
try (RandomAccessFile file = new RandomAccessFile(output.toFile(), "rw");
|
||||
FileChannel channel = file.getChannel()) {
|
||||
for (;;) {
|
||||
try { if (channel.lock() != null) break; } catch (OverlappingFileLockException ignore) {}
|
||||
LockSupport.parkNanos(10_000000);
|
||||
}
|
||||
Registry r = new Registry();
|
||||
r.read(pub, false);
|
||||
r.read(priv, true);
|
||||
r.read(file);
|
||||
var unresolvedErrors = r.addBindings();
|
||||
priv.setLength(0);
|
||||
pub.setLength(0);
|
||||
pub.writeBytes("VERSION " + implVersion + "\n");
|
||||
r.write(pub, priv);
|
||||
file.setLength(0);
|
||||
file.writeBytes("VERSION " + implVersion + "\n");
|
||||
r.write(file);
|
||||
if (!unresolvedErrors.isEmpty()) {
|
||||
throw new RuntimeException(String.join("\n", unresolvedErrors));
|
||||
}
|
||||
|
||||
@@ -45,7 +45,10 @@ public class JBRApiBootstrap {
|
||||
* @return implementation for {@link com.jetbrains.JBR.ServiceApi} interface
|
||||
*/
|
||||
public static synchronized Object bootstrap(MethodHandles.Lookup outerLookup) {
|
||||
System.out.println("JBR API bootstrap in compatibility mode: Object bootstrap(MethodHandles.Lookup)");
|
||||
if (!JBRApi.ENABLED) return null;
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.out.println("JBR API bootstrap in compatibility mode: Object bootstrap(MethodHandles.Lookup)");
|
||||
}
|
||||
Class<?> apiInterface;
|
||||
try {
|
||||
apiInterface = outerLookup.findClass("com.jetbrains.JBR$ServiceApi");
|
||||
|
||||
@@ -52,20 +52,22 @@ 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) {
|
||||
return JBRApi.init(
|
||||
if (!JBRApi.ENABLED) return null;
|
||||
JBRApi.init(
|
||||
null,
|
||||
apiInterface,
|
||||
serviceAnnotation,
|
||||
providedAnnotation,
|
||||
providesAnnotation,
|
||||
knownExtensions,
|
||||
extensionExtractor);
|
||||
return JBRApi.getService(apiInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,29 +46,21 @@ 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<Supplier<MethodHandle>> dynamicCallTargets = new ArrayList<>();
|
||||
final List<DynamicCallTarget> 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;
|
||||
@@ -92,9 +84,10 @@ class AccessContext {
|
||||
}
|
||||
|
||||
void invokeDynamic(MethodType type, Supplier<MethodHandle> futureHandle) {
|
||||
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))));
|
||||
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));
|
||||
}
|
||||
|
||||
void invokeDirect(MethodHandleInfo handleInfo) {
|
||||
|
||||
@@ -37,7 +37,6 @@ 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;
|
||||
@@ -50,8 +49,6 @@ 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,7 +33,10 @@ 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;
|
||||
|
||||
@@ -47,13 +50,11 @@ 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.
|
||||
*/
|
||||
private static final boolean ENABLED = Utils.property("jetbrains.runtime.api.enabled", true);
|
||||
public 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[])}
|
||||
@@ -63,7 +64,7 @@ public class JBRApi {
|
||||
/**
|
||||
* Enable extensive debugging logging. Disabled by default.
|
||||
*/
|
||||
static final boolean VERBOSE = Utils.property("jetbrains.runtime.api.verbose", false);
|
||||
public 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.
|
||||
*/
|
||||
@@ -77,68 +78,54 @@ public class JBRApi {
|
||||
*/
|
||||
private static final boolean EXTEND_REGISTRY = Utils.property("jetbrains.runtime.api.extendRegistry", false);
|
||||
|
||||
private final ProxyRepository proxyRepository;
|
||||
private final Boolean[] supportedExtensions;
|
||||
private final long[] emptyExtensionsBitfield;
|
||||
private final Map<Enum<?>, Class<?>[]> knownExtensions;
|
||||
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 JBRApi(ProxyRepository proxyRepository, Map<Enum<?>, Class<?>[]> knownExtensions) {
|
||||
this.proxyRepository = proxyRepository;
|
||||
if (EXTENSIONS_ENABLED) {
|
||||
this.knownExtensions = knownExtensions;
|
||||
JBRApi.knownExtensions = knownExtensions;
|
||||
JBRApi.extensionExtractor = extensionExtractor;
|
||||
supportedExtensions = new Boolean[
|
||||
knownExtensions.keySet().stream().mapToInt(Enum::ordinal).max().orElse(-1) + 1];
|
||||
emptyExtensionsBitfield = new long[(supportedExtensions.length + 63) / 64];
|
||||
} else {
|
||||
this.knownExtensions = null;
|
||||
supportedExtensions = null;
|
||||
emptyExtensionsBitfield = null;
|
||||
}
|
||||
}
|
||||
|
||||
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("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;
|
||||
public static MethodHandle bindDynamic(Lookup caller, String name, MethodType type) {
|
||||
if (VERBOSE) {
|
||||
System.out.println("Binding call site " + caller.lookupClass().getName() + "#" + name + ": " + type);
|
||||
}
|
||||
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.
|
||||
*/
|
||||
public String getImplVersion() {
|
||||
@Provides("JBR.ServiceApi")
|
||||
public static String getImplVersion() {
|
||||
return proxyRepository.getVersion();
|
||||
}
|
||||
|
||||
@@ -148,7 +135,8 @@ public class JBRApi {
|
||||
* @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
|
||||
* service, but is not directly exposed to user.
|
||||
*/
|
||||
public boolean isExtensionSupported(Enum<?> extension) {
|
||||
@Provides("JBR.ServiceApi")
|
||||
public static boolean isExtensionSupported(Enum<?> extension) {
|
||||
if (!EXTENSIONS_ENABLED) return false;
|
||||
int i = extension.ordinal();
|
||||
if (supportedExtensions[i] == null) {
|
||||
@@ -165,13 +153,30 @@ public class JBRApi {
|
||||
return supportedExtensions[i];
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
return Holder.INSTANCE.getService(interFace);
|
||||
|
||||
return getService(interFace, bitfield, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,36 +184,19 @@ public class JBRApi {
|
||||
* @apiNote this method is a part of internal {@link com.jetbrains.JBR.ServiceApi}
|
||||
* service, but is not directly exposed to user.
|
||||
*/
|
||||
public <T> T getService(Class<T> interFace) {
|
||||
return getService(interFace, new Enum<?>[0]);
|
||||
@Provides("JBR.ServiceApi")
|
||||
public static <T> T getService(Class<T> interFace) {
|
||||
return getService(interFace, emptyExtensionsBitfield, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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")
|
||||
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;
|
||||
public static <T> T getInternalService(Class<T> interFace) {
|
||||
return getService(interFace, emptyExtensionsBitfield, false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T getService(Class<T> interFace, long[] extensions, boolean publicService) {
|
||||
Proxy p = proxyRepository.getProxy(interFace, null);
|
||||
if ((p.getFlags() & Proxy.SERVICE) == 0) {
|
||||
if ((p.getFlags() & Proxy.SERVICE) == 0 || (publicService && (p.getFlags() & Proxy.INTERNAL) != 0)) {
|
||||
if (VERBOSE) {
|
||||
Utils.log(Utils.BEFORE_JBR, System.err, "Warning: Not allowed as a service: " + interFace.getCanonicalName());
|
||||
}
|
||||
@@ -222,7 +210,7 @@ public class JBRApi {
|
||||
}
|
||||
try {
|
||||
MethodHandle constructor = p.getConstructor();
|
||||
return (T) (EXTENSIONS_ENABLED ? constructor.invoke(bitfield) : constructor.invoke());
|
||||
return (T) (EXTENSIONS_ENABLED ? constructor.invoke(extensions) : constructor.invoke());
|
||||
} catch (com.jetbrains.exported.JBRApi.ServiceNotAvailableException | NullPointerException e) {
|
||||
if (VERBOSE) {
|
||||
synchronized (System.err) {
|
||||
@@ -236,15 +224,4 @@ 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,7 +96,9 @@ class Proxy {
|
||||
/**
|
||||
* @see Proxy.Info#flags
|
||||
*/
|
||||
static final int SERVICE = 1;
|
||||
static final int
|
||||
INTERNAL = 1,
|
||||
SERVICE = 2;
|
||||
|
||||
private final Proxy inverse;
|
||||
private final Class<?> interFace, target;
|
||||
|
||||
@@ -75,7 +75,6 @@ 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;
|
||||
@@ -99,7 +98,6 @@ 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;
|
||||
@@ -195,10 +193,10 @@ class ProxyGenerator {
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.out.println("Initializing proxy " + interFace.getName());
|
||||
}
|
||||
if (!accessContext.dynamicCallTargets.isEmpty()) {
|
||||
var table = AccessContext.getDynamicCallTargets(generatedProxy);
|
||||
for (int i = 0; i < table.length; i++) table[i] = accessContext.dynamicCallTargets.get(i);
|
||||
|
||||
for (var t : accessContext.dynamicCallTargets) {
|
||||
JBRApi.dynamicCallTargets.put(new JBRApi.DynamicCallTargetKey(
|
||||
generatedProxy.lookupClass(), t.name(), t.descriptor().descriptorString()
|
||||
), t.futureHandle());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +208,7 @@ class ProxyGenerator {
|
||||
try {
|
||||
Object sericeTarget = service && info.targetLookup != null ? createServiceTarget() : null;
|
||||
generatedProxy = proxyGenLookup.defineHiddenClass(
|
||||
bytecode, false, Lookup.ClassOption.NESTMATE);
|
||||
bytecode, false, Lookup.ClassOption.STRONG, Lookup.ClassOption.NESTMATE);
|
||||
MethodHandle constructor = findConstructor();
|
||||
if (sericeTarget != null) constructor = MethodHandles.insertArguments(constructor, 0, sericeTarget);
|
||||
return constructor;
|
||||
@@ -244,10 +242,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);
|
||||
@@ -267,14 +265,6 @@ 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) {
|
||||
@@ -335,8 +325,8 @@ class ProxyGenerator {
|
||||
|
||||
private void generateMethod(ClassBuilder cb, Method method) {
|
||||
Exception exception = null;
|
||||
Enum<?> extension = EXTENSIONS_ENABLED && proxyRepository.extensionExtractor != null ?
|
||||
proxyRepository.extensionExtractor.apply(method) : null;
|
||||
Enum<?> extension = EXTENSIONS_ENABLED && JBRApi.extensionExtractor != null ?
|
||||
JBRApi.extensionExtractor.apply(method) : null;
|
||||
Mapping.Method methodMapping = mappingContext.getMapping(method);
|
||||
MethodHandle handle;
|
||||
boolean passInstance;
|
||||
|
||||
@@ -35,10 +35,8 @@ 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;
|
||||
@@ -50,26 +48,15 @@ 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;
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
String getVersion() {
|
||||
@@ -80,14 +67,15 @@ 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(this),
|
||||
infoByTarget = entry.inverse != null ? entry.inverse.resolve(this) : null;
|
||||
Proxy.Info infoByInterface = entry.resolve(),
|
||||
infoByTarget = entry.inverse != null ? entry.inverse.resolve() : null;
|
||||
inverseKey = infoByTarget != null && infoByTarget.interfaceLookup != null ?
|
||||
new Key(infoByTarget.interfaceLookup.lookupClass(), inverseSpecialization) : null;
|
||||
if ((infoByInterface == null && infoByTarget == null) ||
|
||||
@@ -135,9 +123,9 @@ class ProxyRepository {
|
||||
|
||||
/**
|
||||
* Registry contains all information about mapping between JBR API interfaces and implementation.
|
||||
* This mapping information can be {@linkplain Entry#resolve(ProxyRepository) resolved} into {@link Proxy.Info}.
|
||||
* This mapping information can be {@linkplain Entry#resolve() resolved} into {@link Proxy.Info}.
|
||||
*/
|
||||
static class Registry {
|
||||
private static class Registry {
|
||||
|
||||
private record StaticKey(String methodName, String targetMethodDescriptor) {}
|
||||
private record StaticValue(String targetType, String targetMethodName) {}
|
||||
@@ -152,12 +140,12 @@ class ProxyRepository {
|
||||
|
||||
private Entry(String type) { this.type = type; }
|
||||
|
||||
private Proxy.Info resolve(ProxyRepository repository) {
|
||||
private Proxy.Info resolve() {
|
||||
if (type == null) return null;
|
||||
Lookup l, t;
|
||||
try {
|
||||
l = resolveType(type, repository.classLoader);
|
||||
t = target != null ? resolveType(target, repository.classLoader) : null;
|
||||
l = resolveType(type, classLoader);
|
||||
t = target != null ? resolveType(target, classLoader) : null;
|
||||
} catch (ClassNotFoundException e) {
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.err.println(type + " not eligible");
|
||||
@@ -178,18 +166,18 @@ class ProxyRepository {
|
||||
return INVALID;
|
||||
}
|
||||
if (target == null) flags |= Proxy.SERVICE;
|
||||
if (needsAnnotation(repository, l.lookupClass())) {
|
||||
if (!hasAnnotation(l.lookupClass(), repository.providedAnnotation)) {
|
||||
if (needsAnnotation(l.lookupClass())) {
|
||||
if (!hasAnnotation(l.lookupClass(), providedAnnotation)) {
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.err.println(type + " not eligible: no @Provided annotation");
|
||||
}
|
||||
return INVALID;
|
||||
}
|
||||
if (!hasAnnotation(l.lookupClass(), repository.serviceAnnotation)) flags &= ~Proxy.SERVICE;
|
||||
if (!hasAnnotation(l.lookupClass(), serviceAnnotation)) flags &= ~Proxy.SERVICE;
|
||||
}
|
||||
Proxy.Info info;
|
||||
if (t != null) {
|
||||
if (needsAnnotation(repository, t.lookupClass()) && !hasAnnotation(t.lookupClass(), repository.providesAnnotation)) {
|
||||
if (needsAnnotation(t.lookupClass()) && !hasAnnotation(t.lookupClass(), providesAnnotation)) {
|
||||
if (JBRApi.VERBOSE) {
|
||||
System.err.println(target + " not eligible: no @Provides annotation");
|
||||
}
|
||||
@@ -203,8 +191,8 @@ class ProxyRepository {
|
||||
String targetType = method.getValue().targetType;
|
||||
String targetMethodName = method.getValue().targetMethodName;
|
||||
try {
|
||||
Lookup lookup = resolveType(targetType, repository.classLoader);
|
||||
MethodType mt = MethodType.fromMethodDescriptorString(targetMethodDescriptor, repository.classLoader);
|
||||
Lookup lookup = resolveType(targetType, classLoader);
|
||||
MethodType mt = MethodType.fromMethodDescriptorString(targetMethodDescriptor, classLoader);
|
||||
MethodHandle handle = lookup.findStatic(lookup.lookupClass(), targetMethodName, mt);
|
||||
info.addStaticMethod(methodName, handle);
|
||||
} catch (ClassNotFoundException | IllegalArgumentException | TypeNotPresentException |
|
||||
@@ -242,41 +230,38 @@ class ProxyRepository {
|
||||
public String toString() { return type; }
|
||||
}
|
||||
|
||||
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 Class<? extends Annotation> serviceAnnotation, providedAnnotation, providesAnnotation;
|
||||
private Module annotationsModule;
|
||||
private ClassLoader classLoader;
|
||||
private final Map<String, Entry> entries = new HashMap<>();
|
||||
private final String version;
|
||||
|
||||
Registry(InputStream inputStream) {
|
||||
private Registry() {
|
||||
try (InputStream registryStream = BootLoader.findResourceAsStream("java.base", "META-INF/jbrapi.registry")) {
|
||||
version = readEntries(registryStream);
|
||||
} 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 String readEntries(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 "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 -> {
|
||||
case "TYPE" -> {
|
||||
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)) {
|
||||
@@ -289,7 +274,7 @@ class ProxyRepository {
|
||||
b.inverse = a;
|
||||
a.target = tokens[2];
|
||||
b.target = tokens[1];
|
||||
switch (tokens[0]) {
|
||||
switch (tokens[3]) {
|
||||
case "SERVICE" -> {
|
||||
a.type = null;
|
||||
b.flags |= Proxy.SERVICE;
|
||||
@@ -297,17 +282,56 @@ 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;
|
||||
}
|
||||
this.version = version;
|
||||
return version;
|
||||
}
|
||||
|
||||
private boolean needsAnnotation(ProxyRepository repository, Class<?> c) {
|
||||
return repository.annotationsModule != null && repository.annotationsModule.equals(c.getModule());
|
||||
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 static boolean hasAnnotation(Class<?> c, Class<? extends Annotation> a) {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* 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-2026 JetBrains s.r.o.
|
||||
* Copyright 2025 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,7 +25,6 @@
|
||||
|
||||
package sun.awt.wl.im.text_input_unstable_v3;
|
||||
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -37,25 +36,6 @@ 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;
|
||||
@@ -70,30 +50,18 @@ 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() throws ConversionException {
|
||||
public JavaPreeditString getPreeditString() {
|
||||
if (cachedResultPreeditString != null) {
|
||||
return cachedResultPreeditString;
|
||||
}
|
||||
|
||||
try {
|
||||
cachedResultPreeditString = doUpdatePreeditString
|
||||
? JavaPreeditString.fromWaylandPreeditString(newPreeditStringUtf8, newPreeditStringCursorBeginUtf8Byte, newPreeditStringCursorEndUtf8Byte)
|
||||
: null;
|
||||
cachedResultPreeditString = doUpdatePreeditString
|
||||
? JavaPreeditString.fromWaylandPreeditString(newPreeditStringUtf8, newPreeditStringCursorBeginUtf8Byte, newPreeditStringCursorEndUtf8Byte)
|
||||
: null;
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
return cachedResultPreeditString;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,28 +77,18 @@ 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() throws ConversionException {
|
||||
public JavaCommitString getCommitString() {
|
||||
if (cachedResultCommitString != null) {
|
||||
return cachedResultCommitString;
|
||||
}
|
||||
|
||||
try {
|
||||
cachedResultCommitString = doUpdateCommitString
|
||||
? JavaCommitString.fromWaylandCommitString(newCommitStringUtf8)
|
||||
: null;
|
||||
cachedResultCommitString = doUpdateCommitString
|
||||
? JavaCommitString.fromWaylandCommitString(newCommitStringUtf8)
|
||||
: null;
|
||||
|
||||
return cachedResultCommitString;
|
||||
} catch (CharacterCodingException err) {
|
||||
throw new ConversionException(
|
||||
err,
|
||||
"Failed to convert zwp_text_input_v3::commit_string(%s) to JavaCommitString",
|
||||
byteArrayToHexArrayString(newCommitStringUtf8)
|
||||
);
|
||||
}
|
||||
return cachedResultCommitString;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,27 +128,4 @@ 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-2026 JetBrains s.r.o.
|
||||
* Copyright 2025 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,13 +25,6 @@
|
||||
|
||||
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) {
|
||||
@@ -41,39 +34,8 @@ record JavaCommitString(String text) {
|
||||
|
||||
public static final JavaCommitString EMPTY = new JavaCommitString("");
|
||||
|
||||
/**
|
||||
* 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());
|
||||
/** Never returns {@code null}. */
|
||||
public static JavaCommitString fromWaylandCommitString(byte[] utf8Bytes) {
|
||||
return new JavaCommitString(Utilities.utf8BytesToJavaString(utf8Bytes));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025-2026 JetBrains s.r.o.
|
||||
* Copyright 2025 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,13 +25,6 @@
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -51,11 +44,10 @@ 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 left from the caret.
|
||||
*
|
||||
* @see #fromWaylandPreeditString(byte[], int, int)
|
||||
* the highlight extends to the right from the caret
|
||||
* (e.g., when the text gets selected with Shift + Left Arrow).
|
||||
*/
|
||||
public record JavaPreeditString(String text, int cursorBeginCodeUnit, int cursorEndCodeUnit) {
|
||||
record JavaPreeditString(String text, int cursorBeginCodeUnit, int cursorEndCodeUnit) {
|
||||
public JavaPreeditString {
|
||||
Objects.requireNonNull(text, "text");
|
||||
}
|
||||
@@ -63,43 +55,13 @@ public record JavaPreeditString(String text, int cursorBeginCodeUnit, int cursor
|
||||
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
|
||||
) 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);
|
||||
) {
|
||||
// Java's UTF-8 -> UTF-16 conversion doesn't like trailing NUL codepoints, so let's trim them
|
||||
final int utf8BytesWithoutNulLength = Utilities.getLengthOfUtf8BytesWithoutTrailingNULs(utf8Bytes);
|
||||
|
||||
// cursorBeginUtf8Byte, cursorEndUtf8Byte normalized relatively to the valid values range.
|
||||
final int fixedCursorBeginUtf8Byte;
|
||||
@@ -107,116 +69,37 @@ public record JavaPreeditString(String text, int cursorBeginCodeUnit, int cursor
|
||||
if (cursorBeginUtf8Byte < 0 || cursorEndUtf8Byte < 0) {
|
||||
fixedCursorBeginUtf8Byte = fixedCursorEndUtf8Byte = -1;
|
||||
} else {
|
||||
// 0 <= cursorBeginUtf8Byte <= fixedCursorBeginUtf8Byte <= utf8BytesCorrectedLength
|
||||
fixedCursorBeginUtf8Byte = Math.min(cursorBeginUtf8Byte, utf8BytesCorrectedLength);
|
||||
// 0 <= cursorEndUtf8Byte <= fixedCursorEndUtf8Byte <= utf8BytesCorrectedLength
|
||||
fixedCursorEndUtf8Byte = Math.min(cursorEndUtf8Byte, utf8BytesCorrectedLength);
|
||||
// 0 <= cursorBeginUtf8Byte <= fixedCursorBeginUtf8Byte <= utf8BytesWithoutNulLength
|
||||
fixedCursorBeginUtf8Byte = Math.min(cursorBeginUtf8Byte, utf8BytesWithoutNulLength);
|
||||
// 0 <= cursorEndUtf8Byte <= fixedCursorEndUtf8Byte <= utf8BytesWithoutNulLength
|
||||
fixedCursorEndUtf8Byte = Math.min(cursorEndUtf8Byte, 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;
|
||||
final var resultText = Utilities.utf8BytesToJavaString(utf8Bytes, 0, utf8BytesWithoutNulLength);
|
||||
|
||||
if (fixedCursorBeginUtf8Byte < 0 || fixedCursorEndUtf8Byte < 0) {
|
||||
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();
|
||||
}
|
||||
return new JavaPreeditString(resultText, -1, -1);
|
||||
}
|
||||
|
||||
return new JavaPreeditString(resultText, resultCursorBeginCodeUnit, resultCursorEndCodeUnit);
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025-2026 JetBrains s.r.o.
|
||||
* Copyright 2025 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,8 @@
|
||||
|
||||
package sun.awt.wl.im.text_input_unstable_v3;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
|
||||
interface Utilities {
|
||||
static int getLengthOfUtf8BytesWithoutTrailingNULs(final byte[] utf8Bytes) {
|
||||
@@ -38,34 +40,20 @@ interface Utilities {
|
||||
return (lastNonNulIndex < 0) ? 0 : lastNonNulIndex + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
static String utf8BytesToJavaString(final byte[] utf8Bytes) {
|
||||
if (utf8Bytes == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (index == utf8StrBytesLength) {
|
||||
return true;
|
||||
}
|
||||
return utf8BytesToJavaString(
|
||||
utf8Bytes,
|
||||
0,
|
||||
// Java's UTF-8 -> UTF-16 conversion doesn't like trailing NUL codepoints, so let's trim them
|
||||
getLengthOfUtf8BytesWithoutTrailingNULs(utf8Bytes)
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
static String utf8BytesToJavaString(final byte[] utf8Bytes, final int offset, final int length) {
|
||||
return utf8Bytes == null ? "" : new String(utf8Bytes, offset, length, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025-2026 JetBrains s.r.o.
|
||||
* Copyright 2025 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,12 +659,18 @@ final class WLInputMethodZwpTextInputV3 extends InputMethodAdapter {
|
||||
final int highlightEndCodeUnitIndex =
|
||||
Math.max(0, Math.min(preeditString.cursorEndCodeUnit(), preeditString.text().length()));
|
||||
|
||||
imeCaret = TextHitInfo.beforeOffset(highlightBeginCodeUnitIndex);
|
||||
// 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);
|
||||
|
||||
// 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 (highlightBeginCodeUnitIndex == highlightEndCodeUnitIndex) {
|
||||
if (highlightEndCodeUnitIndex == highlightBeginCodeUnitIndex) {
|
||||
// Only basic highlighting
|
||||
awtInstallIMHighlightingInto(imeText, commitString.text().length(), preeditString.text().length(), 0, 0);
|
||||
} else {
|
||||
@@ -739,6 +745,8 @@ 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) {
|
||||
@@ -1196,29 +1204,6 @@ 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";
|
||||
@@ -1335,9 +1320,9 @@ final class WLInputMethodZwpTextInputV3 extends InputMethodAdapter {
|
||||
}
|
||||
|
||||
if (wlInputContextState.getCurrentWlSurfacePtr() != leftWlSurfacePtr) {
|
||||
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()));
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1438,34 +1423,8 @@ final class WLInputMethodZwpTextInputV3 extends InputMethodAdapter {
|
||||
preeditStringToApply = PropertiesInitials.PREEDIT_STRING;
|
||||
commitStringToApply = PropertiesInitials.COMMIT_STRING;
|
||||
} else {
|
||||
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);
|
||||
preeditStringToApply = Objects.requireNonNullElse(incomingChangesToApply.getPreeditString(), PropertiesInitials.PREEDIT_STRING);
|
||||
commitStringToApply = Objects.requireNonNullElse(incomingChangesToApply.getCommitString(), PropertiesInitials.COMMIT_STRING);
|
||||
}
|
||||
|
||||
this.wlInputContextState.syncWithAppliedIncomingChanges(preeditStringToApply, commitStringToApply, doneSerial);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
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
|
||||
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
|
||||
STATIC MethodMappingTest main ([Ljava/lang/String;)V com.jetbrains.test.api.MethodMapping.Plain c
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PROVIDES absentImpl com.jetbrains.test.api.ProxyInfoResolving.InterfaceWithoutImplementation
|
||||
TYPE absentImpl com.jetbrains.test.api.ProxyInfoResolving.InterfaceWithoutImplementation PROVIDES
|
||||
STATIC NoClass foo ()V com.jetbrains.test.api.ProxyInfoResolving.ServiceWithoutImplementation foo
|
||||
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
|
||||
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
|
||||
STATIC NoClass foo ()V com.jetbrains.test.api.ProxyInfoResolving.ServiceWithExtension foo
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
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;
|
||||
@@ -43,25 +41,11 @@ import static com.jetbrains.test.api.Real.*;
|
||||
|
||||
public class RealTest {
|
||||
|
||||
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}));
|
||||
public static void main(String[] args) {
|
||||
init("RealTest", Map.of(Extensions.FOO, new Class[] {Proxy.class}, Extensions.BAR, new Class[] {Proxy.class}));
|
||||
|
||||
// Get service
|
||||
Service service = Objects.requireNonNull(api.getService(Service.class));
|
||||
Service service = Objects.requireNonNull(JBRApi.getService(Service.class));
|
||||
|
||||
// Proxy passthrough
|
||||
Proxy p = Objects.requireNonNull(service.getProxy());
|
||||
@@ -102,10 +86,10 @@ public class RealTest {
|
||||
}
|
||||
|
||||
// Check extensions
|
||||
if (!api.isExtensionSupported(Extensions.FOO)) {
|
||||
if (!JBRApi.isExtensionSupported(Extensions.FOO)) {
|
||||
throw new Error("FOO extension must be supported");
|
||||
}
|
||||
if (api.isExtensionSupported(Extensions.BAR)) {
|
||||
if (JBRApi.isExtensionSupported(Extensions.BAR)) {
|
||||
throw new Error("BAR extension must not be supported");
|
||||
}
|
||||
try {
|
||||
@@ -117,10 +101,10 @@ public class RealTest {
|
||||
throw new Error("BAR extension was disabled but call succeeded");
|
||||
} catch (UnsupportedOperationException ignore) {}
|
||||
// foo() must succeed when enabled
|
||||
api.getService(Service.class, Extensions.FOO).getProxy().foo();
|
||||
JBRApi.getService(Service.class, Extensions.FOO).getProxy().foo();
|
||||
// Asking for BAR must return null, as it is not supported
|
||||
requireNull(api.getService(Service.class, Extensions.FOO, Extensions.BAR));
|
||||
requireNull(api.getService(Service.class, Extensions.BAR));
|
||||
requireNull(JBRApi.getService(Service.class, Extensions.FOO, Extensions.BAR));
|
||||
requireNull(JBRApi.getService(Service.class, Extensions.BAR));
|
||||
|
||||
// Test specialized (implicit) List proxy
|
||||
List<Api2Way> list = Objects.requireNonNull(service.testList(null));
|
||||
@@ -149,25 +133,4 @@ 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 @@
|
||||
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
|
||||
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
|
||||
|
||||
@@ -28,7 +28,5 @@ package com.jetbrains;
|
||||
*/
|
||||
public class JBR {
|
||||
|
||||
@Service
|
||||
@Provided
|
||||
public interface ServiceApi {}
|
||||
}
|
||||
|
||||
@@ -32,40 +32,31 @@ 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 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 -> {
|
||||
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 -> {
|
||||
Extension e = m.getAnnotation(Extension.class);
|
||||
return e == null ? null : e.value();
|
||||
});
|
||||
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) {
|
||||
} catch (IOException 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(api);
|
||||
proxyRepository = f.get(null);
|
||||
}
|
||||
return proxyRepository;
|
||||
}
|
||||
|
||||
private static Method getProxy;
|
||||
public static Object getProxy(Class<?> interFace) throws Throwable {
|
||||
var repo = getProxyRepository();
|
||||
if (getProxy == null) {
|
||||
@@ -76,6 +67,7 @@ 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");
|
||||
@@ -84,6 +76,7 @@ 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