mirror of
https://github.com/signalapp/libsignal.git
synced 2024-09-20 12:02:18 +02:00
Use a proc_macro to generate arbitrary FFI/JNI bridging
This commit is contained in:
parent
fe89cb76de
commit
1c897b232d
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -848,11 +848,22 @@ dependencies = [
|
||||
"futures",
|
||||
"jni",
|
||||
"libc",
|
||||
"libsignal-bridge-macros",
|
||||
"libsignal-protocol-rust",
|
||||
"log",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-bridge-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unzip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-ffi"
|
||||
version = "0.1.0"
|
||||
@ -1751,6 +1762,12 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unzip3"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99c0ec316ab08201476c032feb2f94a5c8ece5b209765c1fbc4430dd6e931ad6"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "1.5.2"
|
||||
|
@ -111,18 +111,6 @@ pub unsafe extern "C" fn signal_hkdf_derive(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn signal_address_new(
|
||||
address: *mut *mut ProtocolAddress,
|
||||
name: *const c_char,
|
||||
device_id: c_uint,
|
||||
) -> *mut SignalFfiError {
|
||||
run_ffi_safe(|| {
|
||||
let name = read_c_string(name)?;
|
||||
box_object(address, Ok(ProtocolAddress::new(name, device_id)))
|
||||
})
|
||||
}
|
||||
|
||||
ffi_fn_get_uint32!(signal_address_get_device_id(ProtocolAddress) using
|
||||
|obj: &ProtocolAddress| { Ok(obj.device_id()) });
|
||||
|
||||
|
@ -35,7 +35,11 @@ cbindgen = subprocess.Popen(['cbindgen'], cwd=os.path.join(our_abs_dir, '..'), s
|
||||
stdout = str(stdout.decode('utf8'))
|
||||
stderr = str(stderr.decode('utf8'))
|
||||
|
||||
ignore_this_warning = re.compile(r"WARN: Can't find .*\. This usually means that this type was incompatible or not found\.")
|
||||
ignore_this_warning = re.compile(
|
||||
"("
|
||||
r"WARN: Can't find .*\. This usually means that this type was incompatible or not found\.|"
|
||||
r"WARN: Missing `\[defines\]` entry for `feature = \"jni\"` in cbindgen config\."
|
||||
")")
|
||||
|
||||
unknown_warning = False
|
||||
|
||||
|
@ -26,21 +26,6 @@ type JavaSignedPreKeyStore = jobject;
|
||||
type JavaCiphertextMessage = jobject;
|
||||
type JavaSenderKeyStore = jobject;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_org_signal_client_internal_Native_ProtocolAddress_1New(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
name: JString,
|
||||
device_id: jint,
|
||||
) -> ObjectHandle {
|
||||
run_ffi_safe(&env, || {
|
||||
let name: String = env.get_string(name)?.into();
|
||||
let device_id = jint_to_u32(device_id)?;
|
||||
let address = ProtocolAddress::new(name, device_id);
|
||||
box_object::<ProtocolAddress>(Ok(address))
|
||||
})
|
||||
}
|
||||
|
||||
jni_fn_get_jint!(Java_org_signal_client_internal_Native_ProtocolAddress_1DeviceId(ProtocolAddress) using
|
||||
|obj: &ProtocolAddress| { Ok(obj.device_id()) });
|
||||
|
||||
|
@ -21,13 +21,6 @@ pub unsafe fn native_handle_cast_optional<T>(
|
||||
Ok(Some(&mut *(handle as *mut T)))
|
||||
}
|
||||
|
||||
pub fn jint_to_u32(v: jint) -> Result<u32, SignalJniError> {
|
||||
if v < 0 {
|
||||
return Err(SignalJniError::IntegerOverflow(format!("{} to u32", v)));
|
||||
}
|
||||
Ok(v as u32)
|
||||
}
|
||||
|
||||
pub fn jlong_to_u64(v: jlong) -> Result<u64, SignalJniError> {
|
||||
if v < 0 {
|
||||
return Err(SignalJniError::IntegerOverflow(format!("{} to u64", v)));
|
||||
|
@ -13,6 +13,7 @@ license = "AGPL-3.0-only"
|
||||
[dependencies]
|
||||
libsignal-protocol-rust = { path = "../../protocol" }
|
||||
aes-gcm-siv = { path = "../../aes-gcm-siv" }
|
||||
libsignal-bridge-macros = { path = "macros" }
|
||||
futures = "0.3.7"
|
||||
log = "0.4.11"
|
||||
paste = "1.0"
|
||||
|
20
rust/bridge/shared/macros/Cargo.toml
Normal file
20
rust/bridge/shared/macros/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright (C) 2020 Signal Messenger, LLC.
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
|
||||
[package]
|
||||
name = "libsignal-bridge-macros"
|
||||
version = "0.1.0"
|
||||
authors = ["Jack Lloyd <jack@signal.org>", "Jordan Rose <jrose@signal.org>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "1.0", features = ["full"] }
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
unzip3 = "1.0.0"
|
139
rust/bridge/shared/macros/src/lib.rs
Normal file
139
rust/bridge/shared/macros/src/lib.rs
Normal file
@ -0,0 +1,139 @@
|
||||
//
|
||||
// Copyright 2020 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
#![feature(box_patterns)]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::*;
|
||||
use syn::*;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use unzip3::Unzip3;
|
||||
|
||||
fn ffi_bridge_fn(name: String, sig: &Signature) -> TokenStream2 {
|
||||
let name = format_ident!("signal_{}", name);
|
||||
|
||||
let (output_args, output_processing) = match sig.output {
|
||||
ReturnType::Default => (quote!(), quote!()),
|
||||
ReturnType::Type(_, ref ty) => (
|
||||
quote!(out: *mut ffi_result_type!(#ty),), // note the trailing comma
|
||||
quote!(<#ty as ffi::ResultTypeInfo>::write_to(out, __result)?)
|
||||
)
|
||||
};
|
||||
|
||||
let (input_names, input_args, input_processing): (Vec<Ident>, Vec<TokenStream2>, Vec<TokenStream2>) = sig.inputs.iter().map(|arg| match arg {
|
||||
FnArg::Receiver(tokens) => (
|
||||
Ident::new("self", tokens.self_token.span),
|
||||
Error::new(tokens.self_token.span, "cannot have 'self' parameter").to_compile_error(),
|
||||
quote!()
|
||||
),
|
||||
FnArg::Typed(PatType { attrs, pat: box Pat::Ident(name), colon_token, ty: ty @ box Type::Reference(TypeReference { elem: box Type::Array(_), .. }) }) => {
|
||||
let size_arg = format_ident!("{}_size", name.ident);
|
||||
(
|
||||
name.ident.clone(),
|
||||
quote!(#(#attrs)* #name #colon_token ffi_arg_type!(#ty), #size_arg: usize),
|
||||
quote!(let #name = <#ty as ffi::SizedArgTypeInfo>::convert_from(#name, #size_arg)?),
|
||||
)
|
||||
}
|
||||
FnArg::Typed(PatType { attrs, pat: box Pat::Ident(name), colon_token, ty }) => (
|
||||
name.ident.clone(),
|
||||
quote!(#(#attrs)* #name #colon_token ffi_arg_type!(#ty)),
|
||||
quote!(let #name = <#ty as ffi::ArgTypeInfo>::convert_from(#name)?),
|
||||
),
|
||||
FnArg::Typed(PatType { pat, .. }) => (
|
||||
Ident::new("unexpected", pat.span()),
|
||||
Error::new(pat.span(), "cannot use patterns in paramater").to_compile_error(),
|
||||
quote!()
|
||||
)
|
||||
}).unzip3();
|
||||
|
||||
let orig_name = sig.ident.clone();
|
||||
|
||||
quote! {
|
||||
#[cfg(feature = "ffi")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn #name(
|
||||
#output_args
|
||||
#(#input_args),*
|
||||
) -> *mut ffi::SignalFfiError {
|
||||
ffi::run_ffi_safe(|| {
|
||||
#(#input_processing);*;
|
||||
let __result = #orig_name(#(#input_names),*);
|
||||
#output_processing;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn jni_bridge_fn(name: String, sig: &Signature) -> TokenStream2 {
|
||||
let name = format_ident!("Java_org_signal_client_internal_Native_{}", name);
|
||||
|
||||
let output = match sig.output {
|
||||
ReturnType::Default => quote!(),
|
||||
ReturnType::Type(_, ref ty) => quote!(-> jni_result_type!(#ty)),
|
||||
};
|
||||
|
||||
let (input_names, input_args, input_processing): (Vec<Ident>, Vec<TokenStream2>, Vec<TokenStream2>) = sig.inputs.iter().map(|arg| match arg {
|
||||
FnArg::Receiver(tokens) => (
|
||||
Ident::new("self", tokens.self_token.span),
|
||||
Error::new(tokens.self_token.span, "cannot have 'self' parameter").to_compile_error(),
|
||||
quote!()
|
||||
),
|
||||
FnArg::Typed(PatType { attrs, pat: box Pat::Ident(name), colon_token, ty }) => (
|
||||
name.ident.clone(),
|
||||
quote!(#(#attrs)* #name #colon_token jni_arg_type!(#ty)),
|
||||
quote!(let #name = <#ty as jni::ArgTypeInfo>::convert_from(&env, #name)?),
|
||||
),
|
||||
FnArg::Typed(PatType { pat, .. }) => (
|
||||
Ident::new("unexpected", pat.span()),
|
||||
Error::new(pat.span(), "cannot use patterns in paramater").to_compile_error(),
|
||||
quote!()
|
||||
)
|
||||
}).unzip3();
|
||||
|
||||
let orig_name = sig.ident.clone();
|
||||
|
||||
quote! {
|
||||
#[cfg(feature = "jni")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn #name(
|
||||
env: jni::JNIEnv,
|
||||
_class: jni::JClass,
|
||||
#(#input_args),*
|
||||
) #output {
|
||||
jni::run_ffi_safe(&env, || {
|
||||
#(#input_processing);*;
|
||||
jni::ResultTypeInfo::convert_into(#orig_name(#(#input_names),*), &env)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn bridge_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let function = parse_macro_input!(item as ItemFn);
|
||||
|
||||
let item_names = parse_macro_input!(attr with Punctuated<MetaNameValue, Token![,]>::parse_terminated);
|
||||
let ffi_name = match item_names.iter().find(|meta| meta.path.get_ident().map_or(false, |ident| ident == "ffi")) {
|
||||
Some(MetaNameValue { lit: Lit::Str(name_str), .. }) => name_str.value(),
|
||||
Some(meta) => return Error::new(meta.lit.span(), "ffi name must be a string literal").to_compile_error().into(),
|
||||
None => function.sig.ident.to_string(),
|
||||
};
|
||||
let jni_name = match item_names.iter().find(|meta| meta.path.get_ident().map_or(false, |ident| ident == "jni")) {
|
||||
Some(MetaNameValue { lit: Lit::Str(name_str), .. }) => name_str.value(),
|
||||
Some(meta) => return Error::new(meta.lit.span(), "jni name must be a string literal").to_compile_error().into(),
|
||||
None => function.sig.ident.to_string(),
|
||||
};
|
||||
|
||||
let ffi_fn = ffi_bridge_fn(ffi_name, &function.sig);
|
||||
let jni_fn = jni_bridge_fn(jni_name, &function.sig);
|
||||
|
||||
let mut result = function.into_token_stream();
|
||||
result.extend(ffi_fn);
|
||||
result.extend(jni_fn);
|
||||
result.into()
|
||||
}
|
95
rust/bridge/shared/src/ffi/convert.rs
Normal file
95
rust/bridge/shared/src/ffi/convert.rs
Normal file
@ -0,0 +1,95 @@
|
||||
//
|
||||
// Copyright 2020 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
use libc::{c_char, c_uchar};
|
||||
use libsignal_protocol_rust::*;
|
||||
use std::ffi::CStr;
|
||||
|
||||
use crate::ffi::error::*;
|
||||
|
||||
pub(crate) trait ArgTypeInfo: Sized {
|
||||
type ArgType;
|
||||
fn convert_from(foreign: Self::ArgType) -> Result<Self, SignalFfiError>;
|
||||
}
|
||||
|
||||
pub(crate) trait SizedArgTypeInfo: Sized {
|
||||
type ArgType;
|
||||
fn convert_from(foreign: Self::ArgType, size: usize) -> Result<Self, SignalFfiError>;
|
||||
}
|
||||
|
||||
pub(crate) trait ResultTypeInfo: Sized {
|
||||
type ResultType;
|
||||
fn convert_into(self) -> Result<Self::ResultType, SignalFfiError>;
|
||||
fn write_to(ptr: *mut Self::ResultType, value: Self) -> Result<(), SignalFfiError> {
|
||||
if ptr.is_null() {
|
||||
return Err(SignalFfiError::NullPointer);
|
||||
}
|
||||
unsafe { *ptr = value.convert_into()? };
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait TrivialTypeInfo {}
|
||||
|
||||
impl<T: TrivialTypeInfo> ArgTypeInfo for T {
|
||||
type ArgType = Self;
|
||||
fn convert_from(foreign: Self) -> Result<Self, SignalFfiError> { Ok(foreign) }
|
||||
}
|
||||
|
||||
impl<T: TrivialTypeInfo> ResultTypeInfo for T {
|
||||
type ResultType = Self;
|
||||
fn convert_into(self) -> Result<Self, SignalFfiError> { Ok(self) }
|
||||
}
|
||||
|
||||
impl SizedArgTypeInfo for &[u8] {
|
||||
type ArgType = *const c_uchar;
|
||||
fn convert_from(input: Self::ArgType, input_len: usize) -> Result<Self, SignalFfiError> {
|
||||
if input.is_null() {
|
||||
if input_len != 0 {
|
||||
return Err(SignalFfiError::NullPointer);
|
||||
}
|
||||
// We can't just fall through because slice::from_raw_parts still expects a non-null pointer. Reference a dummy buffer instead.
|
||||
return Ok(&[]);
|
||||
}
|
||||
|
||||
unsafe { Ok(std::slice::from_raw_parts(input, input_len)) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgTypeInfo for String {
|
||||
type ArgType = *const c_char;
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
fn convert_from(foreign: *const c_char) -> Result<Self, SignalFfiError> {
|
||||
if foreign.is_null() {
|
||||
return Err(SignalFfiError::NullPointer);
|
||||
}
|
||||
|
||||
match unsafe { CStr::from_ptr(foreign).to_str() } {
|
||||
Ok(s) => Ok(s.to_owned()),
|
||||
Err(_) => Err(SignalFfiError::InvalidUtf8String),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TrivialTypeInfo for u32 {}
|
||||
impl TrivialTypeInfo for usize {}
|
||||
|
||||
impl ResultTypeInfo for ProtocolAddress {
|
||||
type ResultType = *mut ProtocolAddress;
|
||||
fn convert_into(self) -> Result<Self::ResultType, SignalFfiError> {
|
||||
Ok(Box::into_raw(Box::new(self)))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! ffi_arg_type {
|
||||
(u32) => (u32);
|
||||
(usize) => (libc::size_t);
|
||||
(&[u8]) => (*const libc::c_uchar);
|
||||
(String) => (*const libc::c_char);
|
||||
}
|
||||
|
||||
macro_rules! ffi_result_type {
|
||||
( $typ:ty ) => (*mut $typ);
|
||||
}
|
@ -7,6 +7,10 @@ use libc::{c_char, c_uchar, size_t};
|
||||
use libsignal_protocol_rust::*;
|
||||
use std::ffi::CString;
|
||||
|
||||
#[macro_use]
|
||||
mod convert;
|
||||
pub(crate) use convert::*;
|
||||
|
||||
mod error;
|
||||
pub use error::*;
|
||||
|
||||
|
62
rust/bridge/shared/src/jni/convert.rs
Normal file
62
rust/bridge/shared/src/jni/convert.rs
Normal file
@ -0,0 +1,62 @@
|
||||
//
|
||||
// Copyright 2020 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::JString;
|
||||
use libsignal_protocol_rust::*;
|
||||
|
||||
use crate::jni::*;
|
||||
|
||||
pub(crate) trait ArgTypeInfo<'a>: Sized {
|
||||
type ArgType;
|
||||
fn convert_from(env: &JNIEnv<'a>, foreign: Self::ArgType) -> Result<Self, SignalJniError>;
|
||||
}
|
||||
|
||||
pub(crate) trait ResultTypeInfo<'a>: Sized {
|
||||
type ResultType;
|
||||
fn convert_into(self, env: &JNIEnv<'a>) -> Result<Self::ResultType, SignalJniError>;
|
||||
}
|
||||
|
||||
pub(crate) trait TrivialTypeInfo {}
|
||||
|
||||
impl<'a, T: TrivialTypeInfo> ArgTypeInfo<'a> for T {
|
||||
type ArgType = Self;
|
||||
fn convert_from(_env: &JNIEnv<'a>, foreign: Self) -> Result<Self, SignalJniError> { Ok(foreign) }
|
||||
}
|
||||
|
||||
impl<'a, T: TrivialTypeInfo> ResultTypeInfo<'a> for T {
|
||||
type ResultType = Self;
|
||||
fn convert_into(self, _env: &JNIEnv<'a>) -> Result<Self, SignalJniError> { Ok(self) }
|
||||
}
|
||||
|
||||
impl<'a> ArgTypeInfo<'a> for u32 {
|
||||
type ArgType = jint;
|
||||
fn convert_from(_env: &JNIEnv<'a>, foreign: jint) -> Result<Self, SignalJniError> {
|
||||
jint_to_u32(foreign)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ArgTypeInfo<'a> for String {
|
||||
type ArgType = JString<'a>;
|
||||
fn convert_from(env: &JNIEnv<'a>, foreign: JString<'a>) -> Result<Self, SignalJniError> {
|
||||
Ok(env.get_string(foreign)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ResultTypeInfo<'a> for ProtocolAddress {
|
||||
type ResultType = ObjectHandle;
|
||||
fn convert_into(self, _env: &JNIEnv<'a>) -> Result<Self::ResultType, SignalJniError> {
|
||||
box_object(Ok(self))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! jni_arg_type {
|
||||
(u32) => (jni::jint);
|
||||
(String) => (jni::JString);
|
||||
}
|
||||
|
||||
macro_rules! jni_result_type {
|
||||
( $typ:ty ) => (jni::ObjectHandle);
|
||||
}
|
@ -4,16 +4,20 @@
|
||||
//
|
||||
|
||||
use jni::objects::{JObject, JValue};
|
||||
use jni::sys::{_jobject, jboolean, jint, jlong};
|
||||
use jni::sys::{_jobject, jboolean, jlong};
|
||||
|
||||
use aes_gcm_siv::Error as AesGcmSivError;
|
||||
use libsignal_protocol_rust::*;
|
||||
|
||||
pub(crate) use jni::objects::JClass;
|
||||
pub(crate) use jni::objects::{JClass, JString};
|
||||
pub(crate) use jni::strings::JNIString;
|
||||
pub(crate) use jni::sys::{jbyteArray, jstring};
|
||||
pub(crate) use jni::sys::{jbyteArray, jint, jstring};
|
||||
pub(crate) use jni::JNIEnv;
|
||||
|
||||
#[macro_use]
|
||||
mod convert;
|
||||
pub(crate) use convert::*;
|
||||
|
||||
mod error;
|
||||
pub use error::*;
|
||||
|
||||
@ -200,6 +204,13 @@ pub unsafe fn native_handle_cast<T>(
|
||||
Ok(&mut *(handle as *mut T))
|
||||
}
|
||||
|
||||
pub fn jint_to_u32(v: jint) -> Result<u32, SignalJniError> {
|
||||
if v < 0 {
|
||||
return Err(SignalJniError::IntegerOverflow(format!("{} to u32", v)));
|
||||
}
|
||||
Ok(v as u32)
|
||||
}
|
||||
|
||||
pub fn to_jbytearray<T: AsRef<[u8]>>(
|
||||
env: &JNIEnv,
|
||||
data: Result<T, SignalProtocolError>,
|
||||
|
@ -6,6 +6,7 @@
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
|
||||
use aes_gcm_siv::Aes256GcmSiv;
|
||||
use libsignal_bridge_macros::*;
|
||||
use libsignal_protocol_rust::*;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
@ -29,6 +30,11 @@ bridge_get_string!(name(ProtocolAddress), ffi = address_get_name =>
|
||||
|p| Ok(p.name())
|
||||
);
|
||||
|
||||
#[bridge_fn(jni = "ProtocolAddress_1New")]
|
||||
fn address_new(name: String, device_id: u32) -> ProtocolAddress {
|
||||
ProtocolAddress::new(name, device_id)
|
||||
}
|
||||
|
||||
bridge_destroy!(PublicKey, ffi = publickey, jni = ECPublicKey);
|
||||
bridge_deserialize!(PublicKey::deserialize, ffi = publickey, jni = None);
|
||||
bridge_get_bytearray!(serialize(PublicKey), ffi = publickey_serialize, jni = ECPublicKey_1Serialize =>
|
||||
|
@ -33,6 +33,15 @@ macro_rules! expr_as_fn {
|
||||
};
|
||||
}
|
||||
|
||||
// macro_rules! bridge_fn {
|
||||
// (ffi = $ffi_name:ident, jni = $jni_name:ident, $($body:tt)+) => {
|
||||
// #[cfg(feature = "ffi")]
|
||||
// ffi_bridge_fn!($ffi_name $($body)+);
|
||||
// // #[cfg(feature = "jni")]
|
||||
// // jni_bridge_get_string!($name($typ) $(as $jni_name)? => $body);
|
||||
// }
|
||||
// }
|
||||
|
||||
macro_rules! bridge_destroy {
|
||||
($typ:ty $(, ffi = $ffi_name:ident)? $(, jni = $jni_name:ident)? ) => {
|
||||
#[cfg(feature = "ffi")]
|
||||
|
Loading…
Reference in New Issue
Block a user