Skip to main content

twips_ffi/
lib.rs

1mod events;
2
3use std::{
4    ffi::{c_char, CStr, CString},
5    ptr::null_mut,
6    str::FromStr,
7};
8
9use twips::scramble::{
10    derive_scramble_for_event_seeded, random_scramble_for_event,
11    scramble_finder::free_memory_for_all_scramble_finders, DerivationSalt, DerivationSeed, Event,
12};
13
14fn unwrap_cstr_result_or_null_ptr(result: Result<*const c_char, ()>) -> *const c_char {
15    result.unwrap_or(null_mut())
16}
17
18fn war_cstr_to_rust_str_ref<'a>(cstr: *const c_char) -> Result<&'a str, ()> {
19    let cstr = unsafe { CStr::from_ptr(cstr) };
20    cstr.to_str().map_err(|_| ())
21}
22
23// TODO: we can't avoid leaking the return value, but we could give a function to free all past returned values.
24fn rust_str_to_raw_cstr(s: &str) -> *const c_char {
25    CString::new(s).unwrap().into_raw()
26}
27
28/// # Safety
29///
30/// This function can panic. If you are working in pure Rust, use [`twips::scramble::random_scramble_for_event`] instead.
31///
32/// Returns:
33/// - A null pointer for *any* error.
34/// - A valid scramble (in the form of a C string) otherwise.
35#[no_mangle]
36pub unsafe extern "C" fn ffi_random_scramble_for_event(
37    event_raw_cstr: *const c_char,
38) -> *const c_char {
39    unwrap_cstr_result_or_null_ptr(ffi_random_scramble_for_event_internal(event_raw_cstr))
40}
41
42fn ffi_random_scramble_for_event_internal(
43    event_raw_cstr: *const c_char,
44) -> Result<*const c_char, ()> {
45    let event_str = war_cstr_to_rust_str_ref(event_raw_cstr)?;
46    let event = Event::try_from(event_str).map_err(|_| ())?;
47    let result_str = random_scramble_for_event(event)
48        .map_err(|_| ())?
49        .to_string();
50    Ok(rust_str_to_raw_cstr(&result_str))
51}
52
53/// # Safety
54///
55/// This function can panic. If you are working in pure Rust, use [`twips::scramble::derive_scramble_for_event`] instead.
56///
57/// Returns:
58/// - A null pointer for *any* error.
59/// - A valid derived scramble (in the form of a C string) otherwise.
60#[no_mangle]
61pub extern "C" fn ffi_derive_scramble_for_event(
62    hex_derivation_seed_cstr: *const c_char,
63    // Blank string or a slash-separated hierarchy
64    derivation_salt_hierarchy_str: *const c_char,
65    subevent_str: *const c_char,
66) -> *const c_char {
67    unwrap_cstr_result_or_null_ptr(ffi_derive_scramble_for_event_internal(
68        hex_derivation_seed_cstr,
69        derivation_salt_hierarchy_str,
70        subevent_str,
71    ))
72}
73
74fn ffi_derive_scramble_for_event_internal(
75    hex_derivation_seed_raw_cstr: *const c_char,
76    // Blank string or a slash-separated hierarchy
77    derivation_salt_hierarchy_raw_cstr: *const c_char,
78    subevent_raw_cstr: *const c_char,
79) -> Result<*const c_char, ()> {
80    let hex_derivation_seed_str = war_cstr_to_rust_str_ref(hex_derivation_seed_raw_cstr)?;
81    let derivation_salt_hierarchy_str =
82        war_cstr_to_rust_str_ref(derivation_salt_hierarchy_raw_cstr)?;
83    let subevent_str = war_cstr_to_rust_str_ref(subevent_raw_cstr)?;
84
85    let derivation_seed = DerivationSeed::from_str(hex_derivation_seed_str).map_err(|_| ())?;
86    let hierarchy = if derivation_salt_hierarchy_str.is_empty() {
87        vec![]
88    } else {
89        derivation_salt_hierarchy_str
90            .split("/")
91            .map(DerivationSalt::from_str)
92            .collect::<Result<Vec<DerivationSalt>, String>>()
93            .map_err(|_| ())?
94    };
95    let subevent = Event::try_from(subevent_str)
96        .map_err(|e| e.description)
97        .map_err(|_| ())?;
98    match derive_scramble_for_event_seeded(&derivation_seed, &hierarchy, subevent) {
99        Ok(scramble) => Ok(rust_str_to_raw_cstr(&scramble.to_string())),
100        Err(_) => Err(()),
101    }
102}
103
104#[no_mangle]
105pub extern "C" fn ffi_free_memory_for_all_scramble_finders() -> u32 {
106    // We cast to `u32` for the public API so that it's more stable across environments (including WASM).
107    // If we've allocated more than `u32::MAX` scramble finders, I'd be *very* impressed.
108    free_memory_for_all_scramble_finders() as u32
109}
110
111#[test]
112fn ffi_test() {
113    // event ID, min num moves (inclusive), max num moves (inclusive)
114    let test_data = [
115        ("222", 11, 13), // TODO: are there any states that can't be reached in exactly 11 moves for our scramble generators?
116        ("pyram", 11, 15), // TODO: are there any states that can't be reached in exactly 11 moves for our scramble generators (ignoring tips)?
117        ("333", 15, 30),
118        ("555", 60, 60),
119        ("666", 80, 80),
120        ("777", 100, 100),
121        ("minx", 83, 83),
122    ];
123
124    let dylib_path = test_cdylib::build_current_project();
125    let lib = unsafe { libloading::Library::new(dylib_path).unwrap() };
126    let func: libloading::Symbol<unsafe extern "C" fn(event_raw_cstr: *mut c_char) -> *mut c_char> =
127        unsafe { lib.get(b"ffi_random_scramble_for_event").unwrap() };
128    for (event_id, min_num_moves, max_num_moves) in test_data {
129        let event_raw_cstr = CString::new((event_id).to_owned()).unwrap().into_raw();
130        let scramble_raw_cstr = unsafe { func(event_raw_cstr) };
131        let scramble_cstr = unsafe { CStr::from_ptr(scramble_raw_cstr) };
132        let scramble_str = scramble_cstr.to_str().map_err(|_| ()).unwrap();
133        let alg = scramble_str.parse::<cubing::alg::Alg>().unwrap();
134        assert!(alg.nodes.len() >= min_num_moves);
135        assert!(alg.nodes.len() <= max_num_moves);
136    }
137}