Change how callbacks work

This commit is contained in:
Bjorn Beishline 2026-01-31 04:47:13 -07:00
parent deb1710c3e
commit 20c823eb76
10 changed files with 145 additions and 207 deletions

View File

@ -17,7 +17,7 @@ dependencies {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
}
}

View File

@ -1,24 +0,0 @@
package org.patchbukkit;
public class CallbackValue {
public String callbackName;
public Object[] args;
public CallbackValue(String callbackName, Object[] args) {
this.callbackName = callbackName;
this.args = args;
}
public Object getArg(Integer arg) {
return args[arg];
}
public Object argLength() {
return args.length;
}
public String getCallbackName() {
return callbackName;
}
}

View File

@ -1,51 +0,0 @@
// No longer used currently, keeping around for reference in the short term
package org.patchbukkit;
import java.util.UUID;
import org.astonbitecode.j4rs.api.invocation.NativeCallbackToRustChannelSupport;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
public class NativeCallbacks extends NativeCallbackToRustChannelSupport {
private static NativeCallbacks instance;
public NativeCallbacks() {
NativeCallbacks.setInstance(this);
}
private static synchronized void setInstance(
NativeCallbacks nativeCallbacks
) {
if (instance == null) {
instance = nativeCallbacks;
}
}
public static NativeCallbacks getInstance() {
return instance;
}
public void registerEventCallback(
@NotNull Listener listener,
@NotNull Plugin plugin
) {
doCallback(
new CallbackValue(
"REGISTER_EVENT_CALLBACK",
new Object[] { listener, plugin }
)
);
}
public void sendMessage(@NotNull UUID uuid, String message) {
NativeCallbacks.getInstance().doCallback(
new CallbackValue(
"SEND_MESSAGE",
new Object[] { uuid.toString(), message }
)
);
}
}

View File

@ -0,0 +1,55 @@
package org.patchbukkit.bridge;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.util.UUID;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
public class NativePatchBukkit {
private static final Linker LINKER = Linker.nativeLinker();
private static MethodHandle sendMessageNative;
private static MethodHandle registerEventNative;
// Called from Rust during initialization
public static void initCallbacks(long sendMessageAddr, long registerEventAddr) {
// void rust_send_message(const char* uuid, const char* message)
sendMessageNative = LINKER.downcallHandle(
MemorySegment.ofAddress(sendMessageAddr),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
// void rust_register_event(void* listener, void* plugin)
registerEventNative = LINKER.downcallHandle(
MemorySegment.ofAddress(registerEventAddr),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
}
public static void sendMessage(UUID uuid, String message) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment uuidStr = arena.allocateFrom(uuid.toString());
MemorySegment msgStr = arena.allocateFrom(message);
sendMessageNative.invokeExact(uuidStr, msgStr);
} catch (Throwable t) {
throw new RuntimeException("Failed to call native sendMessage", t);
}
}
public static void registerEvent(Listener listener, Plugin plugin) {
// For objects, you have options:
// 1. Pass identifying info (strings, IDs) instead of object refs
// 2. Store objects in a Java-side registry and pass an ID
// 3. Use JNI NewGlobalRef from Rust side if you need to hold refs
// Simple approach - pass plugin name, handle lookup in Rust
try (Arena arena = Arena.ofConfined()) {
MemorySegment pluginName = arena.allocateFrom(plugin.getName());
// ...
} catch (Throwable t) {
throw new RuntimeException("Failed to register event", t);
}
}
}

View File

@ -60,8 +60,8 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import org.jetbrains.annotations.UnmodifiableView;
import org.patchbukkit.NativeCallbacks;
import org.patchbukkit.PatchBukkitServer;
import org.patchbukkit.bridge.NativePatchBukkit;
import com.destroystokyo.paper.ClientOption;
import com.destroystokyo.paper.Title;
@ -97,7 +97,7 @@ public class PatchBukkitPlayer
sender = this.getUniqueId();
}
NativeCallbacks.getInstance().sendMessage(sender, message);
NativePatchBukkit.sendMessage(uuid, message);
}
@Override

View File

@ -1,6 +1,6 @@
use std::sync::{Arc, Mutex};
use j4rs::{Instance, InvocationArg, Jvm};
use j4rs::Instance;
use pumpkin::command::CommandExecutor;
use tokio::sync::{mpsc, oneshot};

View File

@ -5,7 +5,7 @@ use std::{
use anyhow::Result;
use j4rs::Instance;
use pumpkin::{command::CommandSender, plugin::Context, server::Server};
use pumpkin::plugin::Context;
use tokio::sync::{mpsc, oneshot};
use crate::{events::Event, java::jvm::command_executor::SimpleCommandSender};
@ -25,10 +25,6 @@ pub enum JvmCommand {
respond_to: oneshot::Sender<Result<()>>,
context: Arc<Context>,
},
JavaCallback {
instance: Instance,
respond_to: oneshot::Sender<Result<()>>,
},
LoadPlugin {
plugin_path: PathBuf,
respond_to: oneshot::Sender<LoadPluginResult>,

View File

@ -1,16 +1,15 @@
use std::{path::PathBuf, str::FromStr, sync::Arc};
use std::{path::PathBuf, sync::Arc};
use anyhow::bail;
use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder};
use pumpkin::{net::bedrock::play, plugin::Context, server::Server};
use pumpkin_util::text::{TextComponent, color::NamedColor};
use tokio::sync::{mpsc, oneshot};
use j4rs::{InvocationArg, Jvm, JvmBuilder};
use pumpkin::plugin::Context;
use tokio::sync::mpsc;
use crate::{
events::Event,
java::{
jar::read_configs_from_jar,
jvm::commands::{JvmCommand, LoadPluginResult},
native_callbacks::{init_callback_context, initialize_callbacks},
},
plugin::{event_manager::EventManager, manager::PluginManager},
};
@ -49,17 +48,12 @@ impl JvmWorker {
respond_to,
context,
} => {
init_callback_context(context.clone(), tokio::runtime::Handle::current())
.unwrap();
self.context = Some(context);
let result = self.initialize_jvm(&j4rs_path);
let _ = respond_to.send(result);
}
JvmCommand::JavaCallback {
instance,
respond_to,
} => {
let result = self.process_java_callback(instance).await;
let _ = respond_to.send(result);
}
JvmCommand::LoadPlugin {
plugin_path,
respond_to,
@ -253,123 +247,15 @@ impl JvmWorker {
log::info!("JVM worker thread exited");
}
async fn process_java_callback(&mut self, instance: Instance) -> anyhow::Result<()> {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => bail!("JVM not initialized"),
};
let result = jvm.invoke(&instance, "getCallbackName", InvocationArg::empty())?;
let callback_name: String = jvm.to_rust(result)?;
match callback_name.as_str() {
"REGISTER_EVENT_CALLBACK" => {
let listener = jvm.cast(
&jvm.invoke(&instance, "getArg", &[&InvocationArg::try_from(0_i32)?])?,
"org.bukkit.event.Listener",
)?;
let plugin_instance = jvm.cast(
&jvm.invoke(&instance, "getArg", &[&InvocationArg::try_from(1_i32)?])?,
"org.bukkit.plugin.Plugin",
)?;
let plugin_name: String = jvm.to_rust(jvm.invoke(
&plugin_instance,
"getName",
InvocationArg::empty(),
)?)?;
match self.plugin_manager.plugins.get_mut(&plugin_name) {
Some(rust_plugin) => rust_plugin
.listeners
.insert("listener_name".to_string(), listener),
None => todo!(),
};
}
"SEND_MESSAGE" => {
let uuid: String = jvm.to_rust(jvm.cast(
&jvm.invoke(&instance, "getArg", &[&InvocationArg::try_from(0_i32)?])?,
"java.lang.String",
)?)?;
let message: String = jvm.to_rust(jvm.cast(
&jvm.invoke(&instance, "getArg", &[&InvocationArg::try_from(1_i32)?])?,
"java.lang.String",
)?)?;
if let Some(player) = self
.context
.as_ref()
.unwrap()
.server
.get_player_by_uuid(uuid::Uuid::from_str(&uuid).unwrap())
{
player
.send_system_message(&TextComponent::from_legacy_string(&message))
.await;
}
}
_ => log::warn!("Received unknown callback: {:?}", callback_name),
}
Ok(())
}
fn initialize_jvm(&mut self, j4rs_path: &PathBuf) -> anyhow::Result<()> {
log::info!("Initializing JVM with path: {:?}", j4rs_path);
let jvm = JvmBuilder::new().with_base_path(j4rs_path).build()?;
initialize_callbacks(&jvm)?;
setup_patchbukkit_server(&jvm)?;
let callback_instance =
jvm.create_instance("org.patchbukkit.NativeCallbacks", InvocationArg::empty())?;
let callback_receiver = jvm.init_callback_channel(&callback_instance)?;
let command_tx = self.command_tx.clone();
std::thread::Builder::new()
.name("patchbukkit-jvm-callbacks".to_string())
.spawn(move || {
let thread_runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
thread_runtime.block_on(async {
loop {
match callback_receiver.rx().recv() {
Ok(instance) => {
let (sender, receiver) = oneshot::channel();
if command_tx
.send(JvmCommand::JavaCallback {
instance,
respond_to: sender,
})
.await
.is_err()
{
log::info!(
"Command channel closed, stopping callback forwarder"
);
break;
}
match receiver.await {
Ok(Ok(_)) => (),
Ok(Err(e)) => log::error!("{}", e),
Err(e) => log::error!("Callback failed: {}", e),
}
}
Err(e) => {
log::error!("Java callback channel closed: {}", e);
break;
}
}
}
});
})?;
self.jvm = Some(jvm);
log::info!("JVM initialized successfully");

View File

@ -1,3 +1,4 @@
pub mod jar;
pub mod jvm;
pub mod native_callbacks;
pub mod resources;

View File

@ -0,0 +1,75 @@
use std::{
ffi::{CStr, c_char, c_void},
sync::{Arc, OnceLock},
};
use anyhow::Result;
use j4rs::{InvocationArg, Jvm};
use pumpkin::plugin::Context;
use pumpkin_util::text::TextComponent;
static CALLBACK_CONTEXT: OnceLock<CallbackContext> = OnceLock::new();
struct CallbackContext {
pub plugin_context: Arc<Context>,
pub runtime: tokio::runtime::Handle,
}
pub fn init_callback_context(
plugin_context: Arc<Context>,
runtime: tokio::runtime::Handle,
) -> Result<()> {
let context = CallbackContext {
plugin_context,
runtime,
};
CALLBACK_CONTEXT
.set(context)
.map_err(|_| anyhow::anyhow!("Failed to set callback context"))?;
Ok(())
}
pub fn initialize_callbacks(jvm: &Jvm) -> Result<()> {
let send_message_addr = rust_send_message as *const () as i64;
let register_event_addr = rust_register_event as *const () as i64;
jvm.invoke_static(
"org.patchbukkit.bridge.NativePatchBukkit",
"initCallbacks",
&[
InvocationArg::try_from(send_message_addr)?.into_primitive()?,
InvocationArg::try_from(register_event_addr)?.into_primitive()?,
],
)?;
Ok(())
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_send_message(uuid_ptr: *const c_char, message_ptr: *const c_char) {
// This into_owned might not seem to make any sense, but it's necessary to ensure that the string is owned by the Rust code and can move into the runtime.
let uuid_str = unsafe { CStr::from_ptr(uuid_ptr).to_string_lossy().into_owned() };
let message = unsafe { CStr::from_ptr(message_ptr).to_string_lossy().into_owned() };
if let Some(ctx) = CALLBACK_CONTEXT.get() {
let uuid = uuid::Uuid::parse_str(&uuid_str).unwrap();
ctx.runtime.spawn(async move {
let player = ctx.plugin_context.server.get_player_by_uuid(uuid);
if let Some(player) = player {
player
.send_system_message(&TextComponent::from_legacy_string(&message))
.await;
}
});
}
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_register_event(
listener_ptr: *const c_void, // opaque pointer to Java object
plugin_ptr: *const c_void,
) {
// Handle event registration
}