Add partial Java bindings
This commit is contained in:
parent
04af242ca7
commit
d99145133d
|
@ -1,6 +1,5 @@
|
|||
target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
/target/
|
||||
/Cargo.lock
|
||||
|
||||
# CI
|
||||
/.rustup
|
||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -16,9 +16,12 @@ edition = "2021"
|
|||
default = ["rich_presence"]
|
||||
rich_presence = []
|
||||
tokio-parking_lot = ["tokio/parking_lot"]
|
||||
java-bindings = ["lazy_static", "jni", "tokio/rt-multi-thread"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.52"
|
||||
jni = { version = "0.19", optional = true }
|
||||
lazy_static = { version = "1.4", optional = true }
|
||||
tokio = { version = "1.16", features = ["io-util", "net", "sync", "macros", "rt"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -35,3 +38,10 @@ tokio = { version = "1.16", features = [
|
|||
"macros",
|
||||
"parking_lot",
|
||||
] }
|
||||
|
||||
# this is a workaround to not being able to specify either multiple libs or conditional compilation based on crate-type.
|
||||
[[example]]
|
||||
name = "discord_rpc_java"
|
||||
path = "examples/java.rs"
|
||||
crate-type = ["cdylib"]
|
||||
required-features = ["java-bindings"]
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
use discord_rpc_client::java::*;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JString, JObject};
|
||||
|
||||
// can't just put these in src/java.rs and reexport because of some tree-shaking that the compiler does.
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_discord_rpc_DiscordRPC_create<'a>(env: JNIEnv<'a>, class: JClass) -> JObject<'a> {
|
||||
Java_com_discord_rpc_DiscordRPC_create0(env, class).unwrap_or(JObject::null())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_discord_rpc_DiscordRPC_connect(env: JNIEnv, obj: JObject, client_id: JString) -> bool {
|
||||
Java_com_discord_rpc_DiscordRPC_connect0(env, obj, client_id).is_ok()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_discord_rpc_DiscordRPC_setActivity(env: JNIEnv, obj: JObject, j_activity: JObject) -> bool {
|
||||
Java_com_discord_rpc_DiscordRPC_setActivity0(env, obj, j_activity).is_ok()
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
/.bsp/
|
||||
/target/
|
||||
/project/
|
|
@ -0,0 +1,8 @@
|
|||
ThisBuild / organization := "com.discord"
|
||||
ThisBuild / version := "0.2.0"
|
||||
|
||||
lazy val hello = (project in file("."))
|
||||
.settings(
|
||||
name := "discord-rpc"
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public final class Activity {
|
||||
public String state;
|
||||
public String details;
|
||||
public boolean instance;
|
||||
public ActivityTimestamps timestamps;
|
||||
public ActivityAssets assets;
|
||||
public ActivityParty party;
|
||||
public ActivitySecrets secrets;
|
||||
|
||||
public Activity() {
|
||||
this.state = null;
|
||||
this.details = null;
|
||||
this.instance = false;
|
||||
this.timestamps = null;
|
||||
this.assets = null;
|
||||
this.party = null;
|
||||
this.secrets = null;
|
||||
}
|
||||
|
||||
public Activity withState(String state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Activity withDetails(String details) {
|
||||
this.details = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Activity withInstance(boolean instance) {
|
||||
this.instance = instance;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Activity withTimestamps(ActivityTimestamps timestamps) {
|
||||
this.timestamps = timestamps;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Activity withAssets(ActivityAssets assets) {
|
||||
this.assets = assets;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Activity withParty(ActivityParty party) {
|
||||
this.party = party;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Activity withSecrets(ActivitySecrets secrets) {
|
||||
this.secrets = secrets;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Activity copy() {
|
||||
return new Activity().withState(this.state).withDetails(this.details).withInstance(this.instance).withTimestamps(this.timestamps).withAssets(this.assets).withParty(this.party).withSecrets(this.secrets);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public record ActivityAssets(
|
||||
String largeImage,
|
||||
String largeText,
|
||||
String smallImage,
|
||||
String smallText
|
||||
) {}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public record ActivityParty(@Unsigned int id, @Unsigned int minSize, @Unsigned int maxSize) {}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public record ActivitySecrets(String join, String spectate, String game) {}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
public record ActivityTimestamps(@Unsigned Long start, @Unsigned Long end) {}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class DiscordRPC {
|
||||
private final long handle;
|
||||
|
||||
private DiscordRPC(long handle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the new client instance, or null if an error occurred.
|
||||
*/
|
||||
public static native DiscordRPC create();
|
||||
|
||||
public native boolean connect(String clientId);
|
||||
|
||||
public native boolean setActivity(Activity activity);
|
||||
|
||||
static {
|
||||
final var dir = System.getProperty("com.discord.librarypath");
|
||||
if (dir != null) {
|
||||
System.load(dir + File.separator + System.mapLibraryName("discord_rpc"));
|
||||
} else {
|
||||
System.loadLibrary("discord_rpc");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.discord.rpc;
|
||||
|
||||
/**
|
||||
* Marks an integer that will be re-interpreted natively as unsigned. Use Kotlin's unsigned types with these.
|
||||
*/
|
||||
@interface Unsigned {}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
use log::{log, log_enabled, trace, debug, info, warn, error};
|
||||
|
||||
use std::sync::{Arc, MutexGuard};
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JString, JObject, JValue};
|
||||
use jni::signature::{JavaType, Primitive};
|
||||
use jni::sys::jstring;
|
||||
|
||||
pub use jni;
|
||||
|
||||
use crate as drpc;
|
||||
|
||||
const SIG_BOOL: &'static str = "Z";
|
||||
const SIG_INT: &'static str = "I";
|
||||
const SIG_LONG: &'static str = "J";
|
||||
const SIG_NULLABLE_LONG: &'static str = "Ljava/lang/Long;";
|
||||
const SIG_STRING: &'static str = "Ljava/lang/String;";
|
||||
|
||||
const NAME_DISCORD_RPC: &'static str = "com/discord/rpc/DiscordRPC";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RUNTIME: Arc<tokio::runtime::Runtime> = Arc::new(tokio::runtime::Runtime::new().expect("unable to create tokio runtime"));
|
||||
}
|
||||
|
||||
fn debug_and_discard_err<T, E: core::fmt::Debug>(result: Result<T, E>) -> Result<T, ()> {
|
||||
result.map_err(|e| {
|
||||
error!("{:?}", e);
|
||||
()
|
||||
})
|
||||
}
|
||||
|
||||
fn is_null<'b, O>(env: JNIEnv, ref1: O) -> jni::errors::Result<bool> where O: Into<JObject<'b>> {
|
||||
env.is_same_object(ref1, JObject::null())
|
||||
}
|
||||
|
||||
// taking the [`JNIEnv`] as a reference is needed to satisfy the lifetime checker.
|
||||
fn get_client<'a>(env: &'a JNIEnv<'a>, obj: JObject<'a>) -> Result<MutexGuard<'a, drpc::Client>, ()> {
|
||||
if !debug_and_discard_err(env.is_instance_of(obj, NAME_DISCORD_RPC))? {
|
||||
error!("not an instance of DiscordRPC");
|
||||
return Err(())
|
||||
}
|
||||
|
||||
let client = env.get_rust_field::<_, _, drpc::Client>(obj, "handle");
|
||||
debug_and_discard_err(client)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn Java_com_discord_rpc_DiscordRPC_create0<'a>(env: JNIEnv<'a>, _class: JClass) -> Result<JObject<'a>, ()> {
|
||||
let client = drpc::Client::default();
|
||||
|
||||
let jobj = debug_and_discard_err(env.alloc_object(NAME_DISCORD_RPC))?;
|
||||
debug_and_discard_err(env.set_rust_field(jobj, "handle", client))?;
|
||||
Ok(jobj)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn Java_com_discord_rpc_DiscordRPC_connect0(env: JNIEnv, obj: JObject, client_id: JString) -> Result<(), ()> {
|
||||
let client = get_client(&env, obj)?;
|
||||
|
||||
let client_id = debug_and_discard_err(
|
||||
debug_and_discard_err(
|
||||
debug_and_discard_err(env.get_string(client_id))?.to_str()
|
||||
)?.parse::<u64>()
|
||||
)?;
|
||||
|
||||
if let Some(current_client_id) = client.client_id() {
|
||||
if current_client_id != client_id {
|
||||
RUNTIME.block_on(async { client.disconnect().await });
|
||||
}
|
||||
}
|
||||
|
||||
debug_and_discard_err(RUNTIME.block_on(async { client.connect(client_id).await }))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn Java_com_discord_rpc_DiscordRPC_setActivity0(env: JNIEnv, obj: JObject, j_activity: JObject) -> Result<(), ()> {
|
||||
let client = get_client(&env, obj)?;
|
||||
|
||||
let activity = jobject_to_activity(env, j_activity)?;
|
||||
debug_and_discard_err(RUNTIME.block_on(async { client.set_activity(activity).await }))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jobject_to_activity(env: JNIEnv, jobject: JObject) -> Result<drpc::models::Activity, ()> {
|
||||
let j_state = env.get_field(jobject, "state", SIG_STRING).map_err(|_| ())?;
|
||||
let j_details = env.get_field(jobject, "details", SIG_STRING).map_err(|_| ())?;
|
||||
let j_instance = env.get_field(jobject, "instance", SIG_BOOL).map_err(|_| ())?;
|
||||
let j_timestamps = env.get_field(jobject, "timestamps", "Lcom/discord/rpc/ActivityTimestamps;").map_err(|_| ())?;
|
||||
let j_assets = env.get_field(jobject, "assets", "Lcom/discord/rpc/ActivityAssets;").map_err(|_| ())?;
|
||||
let j_party = env.get_field(jobject, "party", "Lcom/discord/rpc/ActivityParty;").map_err(|_| ())?;
|
||||
let j_secrets = env.get_field(jobject, "secrets", "Lcom/discord/rpc/ActivitySecrets;").map_err(|_| ())?;
|
||||
|
||||
let mut activity = drpc::models::Activity::new();
|
||||
if let JValue::Object(obj) = j_state {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
activity = activity.state(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_details {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
activity = activity.details(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
if let JValue::Bool(b) = j_instance {
|
||||
if b != 0 {
|
||||
activity = activity.instance(true);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_timestamps {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
let timestamps = jobject_to_activity_timestamps(env, obj)?;
|
||||
activity = activity.timestamps(|_|timestamps);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_assets {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
let assets = jobject_to_activity_assets(env, obj)?;
|
||||
activity = activity.assets(|_|assets);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_party {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
let party = jobject_to_activity_party(env, obj)?;
|
||||
activity = activity.party(|_|party);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_secrets {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
let secrets = jobject_to_activity_secrets(env, obj)?;
|
||||
activity = activity.secrets(|_|secrets);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(activity)
|
||||
}
|
||||
|
||||
fn jobject_to_activity_timestamps(env: JNIEnv, jobject: JObject) -> Result<drpc::models::ActivityTimestamps, ()> {
|
||||
let j_start = debug_and_discard_err(env.get_field(jobject, "start", SIG_NULLABLE_LONG))?;
|
||||
let j_end = debug_and_discard_err(env.get_field(jobject, "end", SIG_NULLABLE_LONG))?;
|
||||
|
||||
let mut timestamps = drpc::models::ActivityTimestamps::new();
|
||||
if let JValue::Object(obj) = j_start {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
if let JValue::Long(l) = debug_and_discard_err(env.call_method_unchecked(obj, (obj, "longValue", "()J"), JavaType::Primitive(Primitive::Long), &[]))? {
|
||||
timestamps = timestamps.start(l as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_end {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
if let JValue::Long(l) = debug_and_discard_err(env.call_method_unchecked(obj, (obj, "longValue", "()J"), JavaType::Primitive(Primitive::Long), &[]))? {
|
||||
timestamps = timestamps.end(l as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(timestamps)
|
||||
}
|
||||
|
||||
fn jobject_to_activity_assets(env: JNIEnv, jobject: JObject) -> Result<drpc::models::ActivityAssets, ()> {
|
||||
let j_lrg_img = env.get_field(jobject, "largeImage", SIG_STRING).map_err(|_| ())?;
|
||||
let j_lrg_txt = env.get_field(jobject, "largeText", SIG_STRING).map_err(|_| ())?;
|
||||
let j_sml_img = env.get_field(jobject, "smallImage", SIG_STRING).map_err(|_| ())?;
|
||||
let j_sml_txt = env.get_field(jobject, "smallText", SIG_STRING).map_err(|_| ())?;
|
||||
|
||||
let mut assets = drpc::models::ActivityAssets::new();
|
||||
if let JValue::Object(obj) = j_lrg_img {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
assets = assets.large_image(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_lrg_txt {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
assets = assets.large_text(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_sml_img {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
assets = assets.small_image(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_sml_txt {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
assets = assets.small_text(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(assets)
|
||||
}
|
||||
|
||||
fn jobject_to_activity_party(env: JNIEnv, jobject: JObject) -> Result<drpc::models::ActivityParty, ()> {
|
||||
let j_id = env.get_field(jobject, "id", SIG_INT).map_err(|_| ())?;
|
||||
let j_min_size = env.get_field(jobject, "minSize", SIG_INT).map_err(|_| ())?;
|
||||
let j_max_size = env.get_field(jobject, "maxSize", SIG_INT).map_err(|_| ())?;
|
||||
|
||||
let mut party = drpc::models::ActivityParty::new();
|
||||
if let JValue::Int(l) = j_id {
|
||||
party = party.id(l as u32);
|
||||
}
|
||||
if let (JValue::Int(l1), JValue::Int(l2)) = (j_min_size, j_max_size) {
|
||||
party = party.size((l1 as u32, l2 as u32));
|
||||
}
|
||||
|
||||
Ok(party)
|
||||
}
|
||||
|
||||
fn jobject_to_activity_secrets(env: JNIEnv, jobject: JObject) -> Result<drpc::models::ActivitySecrets, ()> {
|
||||
let j_join = env.get_field(jobject, "join", SIG_STRING).map_err(|_| ())?;
|
||||
let j_spectate = env.get_field(jobject, "spectate", SIG_STRING).map_err(|_| ())?;
|
||||
let j_game = env.get_field(jobject, "game", SIG_STRING).map_err(|_| ())?;
|
||||
|
||||
let mut secrets = drpc::models::ActivitySecrets::new();
|
||||
if let JValue::Object(obj) = j_join {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
secrets = secrets.join(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_spectate {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
secrets = secrets.spectate(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
if let JValue::Object(obj) = j_game {
|
||||
if !is_null(env, obj).map_err(|_| ())? {
|
||||
secrets = secrets.game(env.get_string(obj.into()).map_err(|_| ())?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(secrets)
|
||||
}
|
||||
|
|
@ -17,3 +17,6 @@ pub use client::Client;
|
|||
pub use connection::{Connection, SocketConnection};
|
||||
|
||||
pub use error::*;
|
||||
|
||||
#[cfg(feature = "java-bindings")]
|
||||
pub mod java;
|
||||
|
|
Loading…
Reference in New Issue