Add FFI wrapper crate

This commit is contained in:
Oliver Hamlet
2018-10-20 21:13:33 +01:00
parent 0f805d80ea
commit c0bea60673
13 changed files with 808 additions and 10 deletions
+88
View File
@@ -0,0 +1,88 @@
use libc::c_int;
use loot_condition_interpreter::GameType;
#[no_mangle]
pub static LCI_OK: c_int = 0;
#[no_mangle]
pub static LCI_RESULT_FALSE: c_int = 0;
#[no_mangle]
pub static LCI_RESULT_TRUE: c_int = 1;
/// Invalid arguments were given for the function.
#[no_mangle]
pub static LCI_ERROR_INVALID_ARGS: c_int = -1;
/// Something went wrong while parsing the condition expression.
#[no_mangle]
pub static LCI_ERROR_PARSING_ERROR: c_int = -2;
/// Something went wrong while getting the version of an executable.
#[no_mangle]
pub static LCI_ERROR_PE_PARSING_ERROR: c_int = -3;
/// Some sort of I/O error occurred.
#[no_mangle]
pub static LCI_ERROR_IO_ERROR: c_int = -4;
/// Something panicked.
#[no_mangle]
pub static LCI_ERROR_PANICKED: c_int = -5;
/// A thread lock was poisoned.
#[no_mangle]
pub static LCI_ERROR_POISONED_THREAD_LOCK: c_int = -6;
/// Failed to encode string as a C string, e.g. because there was a nul present.
#[no_mangle]
pub static LCI_ERROR_TEXT_ENCODE_FAIL: c_int = -7;
/// Game code for The Elder Scrolls IV: Oblivion.
#[no_mangle]
pub static LCI_GAME_TES4: c_int = GameType::Tes4 as c_int;
/// Game code for The Elder Scrolls V: Skyrim.
#[no_mangle]
pub static LCI_GAME_TES5: c_int = GameType::Tes5 as c_int;
/// Game code for Fallout 3.
#[no_mangle]
pub static LCI_GAME_FO3: c_int = GameType::Fo3 as c_int;
/// Game code for Fallout: New Vegas.
#[no_mangle]
pub static LCI_GAME_FNV: c_int = GameType::Fonv as c_int;
/// Game code for Fallout 4.
#[no_mangle]
pub static LCI_GAME_FO4: c_int = GameType::Fo4 as c_int;
/// Game code for The Elder Scrolls V: Skyrim Special Edition.
#[no_mangle]
pub static LCI_GAME_TES5SE: c_int = GameType::Tes5se as c_int;
/// Game code for The Elder Scrolls V: Skyrim VR.
#[no_mangle]
pub static LCI_GAME_TES5VR: c_int = GameType::Tes5vr as c_int;
/// Game code for Fallout 4 VR.
#[no_mangle]
pub static LCI_GAME_FO4VR: c_int = GameType::Fo4vr as c_int;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn game_constants_should_have_expected_integer_values() {
assert_eq!(0, LCI_GAME_TES4);
assert_eq!(1, LCI_GAME_TES5);
assert_eq!(2, LCI_GAME_TES5SE);
assert_eq!(3, LCI_GAME_TES5VR);
assert_eq!(4, LCI_GAME_FO3);
assert_eq!(5, LCI_GAME_FNV);
assert_eq!(6, LCI_GAME_FO4);
assert_eq!(7, LCI_GAME_FO4VR);
}
}
+97
View File
@@ -0,0 +1,97 @@
use std::ffi::{CStr, CString};
use std::slice;
use libc::{c_char, c_int, size_t};
use loot_condition_interpreter::{Error, GameType};
use super::ERROR_MESSAGE;
use constants::*;
use state::{plugin_crc, plugin_version};
pub fn error(code: c_int, message: &str) -> c_int {
ERROR_MESSAGE.with(|f| {
*f.borrow_mut() = unsafe { CString::from_vec_unchecked(message.as_bytes().to_vec()) }
});
code
}
pub fn handle_error(err: Error) -> c_int {
let code = map_error(&err);
error(code, &format!("{}", err))
}
fn map_error(err: &Error) -> c_int {
match err {
Error::ParsingIncomplete => LCI_ERROR_PARSING_ERROR,
Error::GenericParsingError(_, _) => LCI_ERROR_PARSING_ERROR,
Error::CustomParsingError(_, _) => LCI_ERROR_PARSING_ERROR,
Error::PeParsingError(_, _) => LCI_ERROR_PE_PARSING_ERROR,
Error::IoError(_, _) => LCI_ERROR_IO_ERROR,
}
}
pub fn map_game_type(game_type: c_int) -> Result<GameType, c_int> {
match game_type {
x if x == LCI_GAME_TES4 => Ok(GameType::Tes4),
x if x == LCI_GAME_TES5 => Ok(GameType::Tes5),
x if x == LCI_GAME_TES5SE => Ok(GameType::Tes5se),
x if x == LCI_GAME_TES5VR => Ok(GameType::Tes5vr),
x if x == LCI_GAME_FO3 => Ok(GameType::Fo3),
x if x == LCI_GAME_FNV => Ok(GameType::Fonv),
x if x == LCI_GAME_FO4 => Ok(GameType::Fo4),
x if x == LCI_GAME_FO4VR => Ok(GameType::Fo4vr),
_ => Err(LCI_ERROR_INVALID_ARGS),
}
}
pub unsafe fn to_str<'a>(c_string: *const c_char) -> Result<&'a str, c_int> {
if c_string.is_null() {
Err(error(LCI_ERROR_INVALID_ARGS, "Null pointer passed"))
} else {
CStr::from_ptr(c_string)
.to_str()
.map_err(|_| error(LCI_ERROR_INVALID_ARGS, "Non-UTF-8 string passed"))
}
}
pub unsafe fn to_vec<U, V, F>(array: *const U, array_size: size_t, mapper: F) -> Result<Vec<V>, i32>
where
F: Fn(&U) -> Result<V, i32>,
{
if array.is_null() || array_size == 0 {
Ok(Vec::new())
} else {
slice::from_raw_parts(array, array_size)
.iter()
.map(|c| mapper(c))
.collect()
}
}
pub unsafe fn to_str_vec<'a>(
array: *const *const c_char,
array_size: size_t,
) -> Result<Vec<&'a str>, i32> {
to_vec(array, array_size, |c| to_str(*c))
}
unsafe fn map_plugin_version(c_object: &plugin_version) -> Result<(String, String), i32> {
to_str(c_object.plugin_name)
.and_then(|n| to_str(c_object.version).map(|v| (n.into(), v.into())))
}
pub unsafe fn map_plugin_versions(
plugin_versions: *const plugin_version,
num_plugins: size_t,
) -> Result<Vec<(String, String)>, i32> {
to_vec(plugin_versions, num_plugins, |v| map_plugin_version(v))
}
pub unsafe fn map_plugin_crcs(
plugin_crcs: *const plugin_crc,
num_entries: size_t,
) -> Result<Vec<(String, u32)>, i32> {
to_vec(plugin_crcs, num_entries, |v| {
to_str(v.plugin_name).map(|s| (s.into(), v.crc))
})
}
+94
View File
@@ -0,0 +1,94 @@
extern crate libc;
extern crate loot_condition_interpreter;
mod constants;
mod helpers;
mod state;
use std::cell::RefCell;
use std::error::Error;
use std::ffi::CString;
use std::panic::catch_unwind;
use std::ptr;
use std::str::FromStr;
use libc::{c_char, c_int};
use loot_condition_interpreter::*;
pub use constants::*;
use helpers::{error, handle_error, to_str};
pub use state::*;
thread_local!(static ERROR_MESSAGE: RefCell<CString> = RefCell::default());
#[no_mangle]
pub unsafe extern "C" fn lci_condition_parse(condition: *const c_char) -> c_int {
catch_unwind(|| {
if condition.is_null() {
error(LCI_ERROR_INVALID_ARGS, "Null pointer passed")
} else {
let expression = match to_str(condition) {
Ok(x) => x,
Err(e) => return e,
};
if let Err(e) = Expression::from_str(expression) {
handle_error(e)
} else {
LCI_OK
}
}
}).unwrap_or(LCI_ERROR_PANICKED)
}
#[no_mangle]
pub unsafe extern "C" fn lci_condition_eval(
condition: *const c_char,
state: *mut lci_state,
) -> c_int {
catch_unwind(|| {
if condition.is_null() || state.is_null() {
error(LCI_ERROR_INVALID_ARGS, "Null pointer passed")
} else {
let expression = match to_str(condition) {
Ok(x) => x,
Err(e) => return e,
};
let expression = match Expression::from_str(expression) {
Err(e) => return handle_error(e),
Ok(x) => x,
};
let state = match (*state).0.read() {
Err(e) => return error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()),
Ok(s) => s,
};
match expression.eval(&state) {
Ok(true) => LCI_RESULT_TRUE,
Ok(false) => LCI_RESULT_FALSE,
Err(e) => handle_error(e),
}
}
}).unwrap_or(LCI_ERROR_PANICKED)
}
#[no_mangle]
pub unsafe extern "C" fn lci_get_error_message(message: *mut *const c_char) -> c_int {
catch_unwind(|| {
if message.is_null() {
error(LCI_ERROR_INVALID_ARGS, "Null pointer passed")
} else {
ERROR_MESSAGE.with(|f| {
if f.borrow().as_bytes().is_empty() {
*message = ptr::null();
} else {
*message = f.borrow().as_ptr() as *const i8;
}
});
LCI_OK
}
}).unwrap_or(LCI_ERROR_PANICKED)
}
+199
View File
@@ -0,0 +1,199 @@
use std::error::Error;
use std::panic::catch_unwind;
use std::path::PathBuf;
use std::sync::RwLock;
use libc::{c_char, c_int, size_t, uint32_t};
use loot_condition_interpreter::State;
use constants::*;
use helpers::{error, map_game_type, map_plugin_crcs, map_plugin_versions, to_str, to_str_vec};
#[allow(non_camel_case_types)]
#[no_mangle]
pub struct lci_state(pub RwLock<State>);
#[allow(non_camel_case_types)]
#[no_mangle]
#[repr(C)]
pub struct plugin_version {
pub plugin_name: *const c_char,
pub version: *const c_char,
}
#[allow(non_camel_case_types)]
#[no_mangle]
#[repr(C)]
pub struct plugin_crc {
pub plugin_name: *const c_char,
pub crc: uint32_t,
}
#[no_mangle]
pub unsafe extern "C" fn lci_state_create(
state: *mut *mut lci_state,
game_type: c_int,
data_path: *const c_char,
loot_path: *const c_char,
) -> c_int {
catch_unwind(|| {
if state.is_null() || data_path.is_null() || loot_path.is_null() {
error(LCI_ERROR_INVALID_ARGS, "Null pointer passed")
} else {
let game_type = match map_game_type(game_type) {
Ok(x) => x,
Err(x) => return error(x, "Invalid game specified"),
};
let data_path = match to_str(data_path) {
Ok(x) => PathBuf::from(x),
Err(e) => return e,
};
let loot_path = match to_str(loot_path) {
Ok(x) => PathBuf::from(x),
Err(e) => return e,
};
*state = Box::into_raw(Box::new(lci_state(RwLock::new(State::new(
game_type, data_path, loot_path,
)))));
LCI_OK
}
}).unwrap_or(LCI_ERROR_PANICKED)
}
#[no_mangle]
pub unsafe extern "C" fn lci_state_destroy(state: *mut lci_state) {
if !state.is_null() {
Box::from_raw(state);
}
}
#[no_mangle]
pub unsafe extern "C" fn lci_state_set_active_plugins(
state: *mut lci_state,
plugin_names: *const *const c_char,
num_plugins: size_t,
) -> c_int {
catch_unwind(|| {
if state.is_null() {
error(LCI_ERROR_INVALID_ARGS, "Null state pointer passed")
} else if plugin_names.is_null() && num_plugins != 0 {
error(
LCI_ERROR_INVALID_ARGS,
"Null plugin_names pointer passed but num_plugins is non-zero",
)
} else if !plugin_names.is_null() && num_plugins == 0 {
error(
LCI_ERROR_INVALID_ARGS,
"Non-null plugin_names pointer passed but num_plugins is zero",
)
} else {
let plugins: Vec<&str> = match to_str_vec(plugin_names, num_plugins) {
Ok(x) => x,
Err(e) => return e,
};
let mut state = match (*state).0.write() {
Err(e) => return error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()),
Ok(h) => h,
};
state.set_active_plugins(&plugins);
LCI_OK
}
}).unwrap_or(LCI_ERROR_PANICKED)
}
#[no_mangle]
pub unsafe extern "C" fn lci_state_set_plugin_versions(
state: *mut lci_state,
plugin_versions: *const plugin_version,
num_plugins: size_t,
) -> c_int {
catch_unwind(|| {
if state.is_null() {
error(LCI_ERROR_INVALID_ARGS, "Null state pointer passed")
} else if plugin_versions.is_null() && num_plugins != 0 {
error(
LCI_ERROR_INVALID_ARGS,
"Null plugin_versions pointer passed but num_plugins is non-zero",
)
} else if !plugin_versions.is_null() && num_plugins == 0 {
error(
LCI_ERROR_INVALID_ARGS,
"Non-null plugin_versions pointer passed but num_plugins is zero",
)
} else {
let plugin_versions = match map_plugin_versions(plugin_versions, num_plugins) {
Ok(x) => x,
Err(e) => return e,
};
let mut state = match (*state).0.write() {
Err(e) => return error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()),
Ok(h) => h,
};
state.set_plugin_versions(&plugin_versions);
LCI_OK
}
}).unwrap_or(LCI_ERROR_PANICKED)
}
#[no_mangle]
pub unsafe extern "C" fn lci_state_set_crc_cache(
state: *mut lci_state,
entries: *const plugin_crc,
num_entries: size_t,
) -> c_int {
catch_unwind(|| {
if state.is_null() {
error(LCI_ERROR_INVALID_ARGS, "Null state pointer passed")
} else if entries.is_null() && num_entries != 0 {
error(
LCI_ERROR_INVALID_ARGS,
"Null entries pointer passed but num_entries is non-zero",
)
} else if !entries.is_null() && num_entries == 0 {
error(
LCI_ERROR_INVALID_ARGS,
"Non-null entries pointer passed but num_entries is zero",
)
} else {
let plugin_crcs = match map_plugin_crcs(entries, num_entries) {
Ok(x) => x,
Err(e) => return e,
};
match (*state).0.write() {
Err(e) => error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()),
Ok(mut s) => match s.set_cached_crcs(&plugin_crcs) {
Err(e) => error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()),
Ok(_) => LCI_OK,
},
}
}
}).unwrap_or(LCI_ERROR_PANICKED)
}
#[no_mangle]
pub unsafe extern "C" fn lci_state_clear_condition_cache(state: *mut lci_state) -> c_int {
catch_unwind(|| {
if state.is_null() {
error(LCI_ERROR_INVALID_ARGS, "Null state pointer passed")
} else {
match (*state).0.write() {
Err(e) => error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()),
Ok(mut s) => match s.clear_condition_cache() {
Err(e) => error(LCI_ERROR_POISONED_THREAD_LOCK, e.description()),
Ok(_) => LCI_OK,
},
}
}
}).unwrap_or(LCI_ERROR_PANICKED)
}