From 8c4d6127bbea30b113d84d0b89c2a06cdd7c358b Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:38:56 -0700 Subject: [PATCH] Add in java generation --- java/patchbukkit/build.gradle.kts | 17 ++ java/protoc-gen-ffi/build.gradle.kts | 24 ++ .../patchbukkit/protocgen/FfiGenerator.java | 230 ++++++++++++++++++ java/settings.gradle.kts | 1 + proto/patchbukkit/common/abilities.proto | 3 + proto/patchbukkit/common/types.proto | 3 + proto/patchbukkit/events/event.proto | 15 +- proto/patchbukkit/events/player_events.proto | 7 +- 8 files changed, 292 insertions(+), 8 deletions(-) create mode 100644 java/protoc-gen-ffi/build.gradle.kts create mode 100644 java/protoc-gen-ffi/src/main/java/org/patchbukkit/protocgen/FfiGenerator.java diff --git a/java/patchbukkit/build.gradle.kts b/java/patchbukkit/build.gradle.kts index 8a1e922..45e306c 100644 --- a/java/patchbukkit/build.gradle.kts +++ b/java/patchbukkit/build.gradle.kts @@ -17,8 +17,25 @@ protobuf { protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" } + + plugins { + create("ffi") { + path = "${rootProject.projectDir}/protoc-gen-ffi/build/libs/protoc-gen-ffi.jar" + } + } + + generateProtoTasks { + all().forEach { task -> + task.plugins { + create("ffi") + } + } + } } +tasks.named("generateProto") { + dependsOn(":protoc-gen-ffi:jar") +} repositories { maven { diff --git a/java/protoc-gen-ffi/build.gradle.kts b/java/protoc-gen-ffi/build.gradle.kts new file mode 100644 index 0000000..43ad72a --- /dev/null +++ b/java/protoc-gen-ffi/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + java + application +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.google.protobuf:protobuf-java:3.25.1") +} + +application { + mainClass.set("org.patchbukkit.protocgen.FfiGenerator") +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "org.patchbukkit.protocgen.FfiGenerator" + } + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} diff --git a/java/protoc-gen-ffi/src/main/java/org/patchbukkit/protocgen/FfiGenerator.java b/java/protoc-gen-ffi/src/main/java/org/patchbukkit/protocgen/FfiGenerator.java new file mode 100644 index 0000000..d367f5f --- /dev/null +++ b/java/protoc-gen-ffi/src/main/java/org/patchbukkit/protocgen/FfiGenerator.java @@ -0,0 +1,230 @@ +package org.patchbukkit.protocgen; + +import com.google.protobuf.DescriptorProtos.*; +import com.google.protobuf.compiler.PluginProtos.*; + +import java.io.IOException; +import java.util.*; + +public class FfiGenerator { + + private static final Map typeMap = new HashMap<>(); + + public static void main(String[] args) throws IOException { + CodeGeneratorRequest request = CodeGeneratorRequest.parseFrom(System.in); + CodeGeneratorResponse.Builder response = CodeGeneratorResponse.newBuilder(); + response.setSupportedFeatures(CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL_VALUE); + + // Build type map from all proto files + for (FileDescriptorProto file : request.getProtoFileList()) { + buildTypeMap(file); + } + + // Generate FFI classes for services + for (FileDescriptorProto file : request.getProtoFileList()) { + if (!request.getFileToGenerateList().contains(file.getName())) { + continue; + } + + String javaPackage = getJavaPackage(file); + + for (ServiceDescriptorProto service : file.getServiceList()) { + String className = service.getName() + "Ffi"; + String content = generateFfiClass(service, javaPackage); + String fileName = javaPackage.replace('.', '/') + "/" + className + ".java"; + + response.addFile(CodeGeneratorResponse.File.newBuilder() + .setName(fileName) + .setContent(content) + .build()); + } + } + + response.build().writeTo(System.out); + } + + private static void buildTypeMap(FileDescriptorProto file) { + String protoPackage = file.getPackage(); + String javaPackage = getJavaPackage(file); + boolean multipleFiles = file.getOptions().getJavaMultipleFiles(); + + String javaPrefix = multipleFiles + ? javaPackage + "." + : javaPackage + "." + getJavaOuterClassName(file) + "."; + + for (DescriptorProto message : file.getMessageTypeList()) { + mapType(protoPackage, message.getName(), javaPrefix); + buildNestedTypeMap(message, "." + protoPackage + "." + message.getName(), + javaPrefix + message.getName()); + } + + for (EnumDescriptorProto enumType : file.getEnumTypeList()) { + mapType(protoPackage, enumType.getName(), javaPrefix); + } + } + + private static void mapType(String protoPackage, String name, String javaPrefix) { + typeMap.put("." + protoPackage + "." + name, javaPrefix + name); + } + + private static void buildNestedTypeMap(DescriptorProto parent, String protoPrefix, String javaPrefix) { + for (DescriptorProto nested : parent.getNestedTypeList()) { + typeMap.put(protoPrefix + "." + nested.getName(), javaPrefix + "." + nested.getName()); + buildNestedTypeMap(nested, protoPrefix + "." + nested.getName(), + javaPrefix + "." + nested.getName()); + } + + for (EnumDescriptorProto enumType : parent.getEnumTypeList()) { + typeMap.put(protoPrefix + "." + enumType.getName(), javaPrefix + "." + enumType.getName()); + } + } + + private static String getJavaPackage(FileDescriptorProto file) { + return file.getOptions().hasJavaPackage() + ? file.getOptions().getJavaPackage() + : file.getPackage(); + } + + private static String getJavaOuterClassName(FileDescriptorProto file) { + if (file.getOptions().hasJavaOuterClassname()) { + return file.getOptions().getJavaOuterClassname(); + } + String name = file.getName(); + int lastSlash = name.lastIndexOf('/'); + if (lastSlash >= 0) { + name = name.substring(lastSlash + 1); + } + return toPascalCase(name.replace(".proto", "")); + } + + private static String generateFfiClass(ServiceDescriptorProto service, String javaPackage) { + StringBuilder sb = new StringBuilder(); + String className = service.getName() + "Ffi"; + List methods = service.getMethodList(); + + // Package and imports + sb.append("package ").append(javaPackage).append(";\n\n"); + sb.append("import java.lang.foreign.*;\n"); + sb.append("import java.lang.invoke.MethodHandle;\n"); + sb.append("import com.google.protobuf.InvalidProtocolBufferException;\n\n"); + + // Class + sb.append("public class ").append(className).append(" {\n\n"); + sb.append(" private static final Linker LINKER = Linker.nativeLinker();\n"); + sb.append(" private static MethodHandle freeNative;\n"); + + for (MethodDescriptorProto method : methods) { + sb.append(" private static MethodHandle ").append(toHandleName(method)).append(";\n"); + } + sb.append("\n"); + + // Init methods + generateInitMethod(sb, methods); + generateInitFreeMethod(sb); + + // Service methods + for (MethodDescriptorProto method : methods) { + generateServiceMethod(sb, method); + } + + sb.append("}\n"); + return sb.toString(); + } + + private static void generateInitMethod(StringBuilder sb, List methods) { + sb.append(" public static void init("); + + StringJoiner params = new StringJoiner(", "); + for (MethodDescriptorProto method : methods) { + params.add("long " + toCamelCase(method.getName()) + "Addr"); + } + sb.append(params).append(") {\n"); + + for (MethodDescriptorProto method : methods) { + String handleName = toHandleName(method); + String addrName = toCamelCase(method.getName()) + "Addr"; + + sb.append(" ").append(handleName).append(" = LINKER.downcallHandle(\n"); + sb.append(" MemorySegment.ofAddress(").append(addrName).append("),\n"); + sb.append(" FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, "); + sb.append("ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));\n"); + } + sb.append(" }\n\n"); + } + + private static void generateInitFreeMethod(StringBuilder sb) { + sb.append(" public static void initFree(long freeAddr) {\n"); + sb.append(" freeNative = LINKER.downcallHandle(\n"); + sb.append(" MemorySegment.ofAddress(freeAddr),\n"); + sb.append(" FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG));\n"); + sb.append(" }\n\n"); + } + + private static void generateServiceMethod(StringBuilder sb, MethodDescriptorProto method) { + String methodName = toCamelCase(method.getName()); + String inputType = resolveJavaType(method.getInputType()); + String outputType = resolveJavaType(method.getOutputType()); + + sb.append(" public static ").append(outputType).append(" ").append(methodName); + sb.append("(").append(inputType).append(" request) {\n"); + sb.append(" try (Arena arena = Arena.ofConfined()) {\n"); + sb.append(" byte[] inputBytes = request.toByteArray();\n"); + sb.append(" MemorySegment inputSegment = arena.allocate(inputBytes.length);\n"); + sb.append(" inputSegment.copyFrom(MemorySegment.ofArray(inputBytes));\n"); + sb.append(" MemorySegment outputLenSegment = arena.allocate(ValueLayout.JAVA_LONG);\n\n"); + + sb.append(" MemorySegment resultPtr = (MemorySegment) ").append(toHandleName(method)); + sb.append(".invokeExact(inputSegment, (long) inputBytes.length, outputLenSegment);\n\n"); + + sb.append(" if (resultPtr.equals(MemorySegment.NULL)) return null;\n\n"); + + sb.append(" long outputLen = outputLenSegment.get(ValueLayout.JAVA_LONG, 0);\n"); + sb.append(" byte[] outputBytes = resultPtr.reinterpret(outputLen).toArray(ValueLayout.JAVA_BYTE);\n\n"); + + sb.append(" try {\n"); + sb.append(" return ").append(outputType).append(".parseFrom(outputBytes);\n"); + sb.append(" } finally {\n"); + sb.append(" freeNative.invokeExact(resultPtr, outputLen);\n"); + sb.append(" }\n"); + sb.append(" } catch (InvalidProtocolBufferException e) {\n"); + sb.append(" throw new RuntimeException(\"Failed to parse response\", e);\n"); + sb.append(" } catch (Throwable t) {\n"); + sb.append(" throw new RuntimeException(\"FFI call failed\", t);\n"); + sb.append(" }\n"); + sb.append(" }\n\n"); + } + + private static String resolveJavaType(String protoType) { + if (typeMap.containsKey(protoType)) { + return typeMap.get(protoType); + } + if (protoType.startsWith(".google.protobuf.")) { + return "com.google.protobuf." + protoType.substring(".google.protobuf.".length()); + } + return protoType.startsWith(".") ? protoType.substring(1) : protoType; + } + + private static String toHandleName(MethodDescriptorProto method) { + return toCamelCase(method.getName()) + "Native"; + } + + private static String toCamelCase(String name) { + return name.isEmpty() ? name : Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + + private static String toPascalCase(String name) { + StringBuilder result = new StringBuilder(); + boolean capitalizeNext = true; + for (char c : name.toCharArray()) { + if (c == '_' || c == '-') { + capitalizeNext = true; + } else if (capitalizeNext) { + result.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + result.append(c); + } + } + return result.toString(); + } +} diff --git a/java/settings.gradle.kts b/java/settings.gradle.kts index db4a198..4693885 100644 --- a/java/settings.gradle.kts +++ b/java/settings.gradle.kts @@ -12,3 +12,4 @@ plugins { rootProject.name = "patchbukkit" include("patchbukkit") +include("protoc-gen-ffi") diff --git a/proto/patchbukkit/common/abilities.proto b/proto/patchbukkit/common/abilities.proto index 207dfeb..7dd9220 100644 --- a/proto/patchbukkit/common/abilities.proto +++ b/proto/patchbukkit/common/abilities.proto @@ -4,6 +4,9 @@ package patchbukkit.common; import "patchbukkit/common/types.proto"; +option java_multiple_files = true; +option java_package = "patchbukkit.common"; + message SetAbilitiesRequest { patchbukkit.common.UUID uuid = 1; patchbukkit.common.Abilities abilities = 2; diff --git a/proto/patchbukkit/common/types.proto b/proto/patchbukkit/common/types.proto index b7d9a19..fca30a2 100644 --- a/proto/patchbukkit/common/types.proto +++ b/proto/patchbukkit/common/types.proto @@ -2,6 +2,9 @@ syntax = "proto3"; package patchbukkit.common; +option java_multiple_files = true; +option java_package = "patchbukkit.common"; + message UUID { string value = 1; } diff --git a/proto/patchbukkit/events/event.proto b/proto/patchbukkit/events/event.proto index f65cbfa..da51626 100644 --- a/proto/patchbukkit/events/event.proto +++ b/proto/patchbukkit/events/event.proto @@ -4,11 +4,14 @@ package patchbukkit.events; import "patchbukkit/events/player_events.proto"; -message Event { - string event_id = 1; - bool cancellable = 2; +option java_multiple_files = true; +option java_package = "patchbukkit.events"; - oneof event_data { - PlayerJoinEvent player_join = 10; - } +message Event { + string event_id = 1; + bool cancellable = 2; + + oneof event_data { + PlayerJoinEvent player_join = 10; + } } diff --git a/proto/patchbukkit/events/player_events.proto b/proto/patchbukkit/events/player_events.proto index 843fb42..bf5cca8 100644 --- a/proto/patchbukkit/events/player_events.proto +++ b/proto/patchbukkit/events/player_events.proto @@ -3,7 +3,10 @@ package patchbukkit.events; import "patchbukkit/common/types.proto"; +option java_multiple_files = true; +option java_package = "patchbukkit.events"; + message PlayerJoinEvent { - patchbukkit.common.UUID player = 1; - string join_message = 2; + patchbukkit.common.UUID player = 1; + string join_message = 2; }