Add in java generation

This commit is contained in:
Bjorn Beishline 2026-02-03 14:38:56 -07:00
parent a31da8c568
commit 8c4d6127bb
8 changed files with 292 additions and 8 deletions

View File

@ -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 {

View File

@ -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
}

View File

@ -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<String, String> 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<MethodDescriptorProto> 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<MethodDescriptorProto> 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();
}
}

View File

@ -12,3 +12,4 @@ plugins {
rootProject.name = "patchbukkit"
include("patchbukkit")
include("protoc-gen-ffi")

View File

@ -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;

View File

@ -2,6 +2,9 @@ syntax = "proto3";
package patchbukkit.common;
option java_multiple_files = true;
option java_package = "patchbukkit.common";
message UUID {
string value = 1;
}

View File

@ -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;
}
}

View File

@ -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;
}