Asynchronous code is executed via special runtime, for python and rust these are the different and incompatible libraries. There you cannot simply share future between languages, it must be run in the same language where it have been created.
As for your example, that means you need to run a Client
in rust executor (e.g. in tokio) and then have a feedback from it. As the simplest way you can just create a global one:
use lazy_static::lazy_static;
use tokio::runtime::Runtime;
lazy_static! {
static ref RUNTIME: Runtime = Runtime::new().unwrap();
}
Then after spawning you need to have a feedback, so you may use a couple of maps with statuses and results:
use std::collections::HashMap;
use std::sync::RwLock;
use futures::prelude::*;
use tokio::sync::oneshot;
type FutureId = u64;
type UrlResult = reqwest::Result<String>;
type SyncMap<K, V> = RwLock<HashMap<K, V>>;
lazy_static! {
// Map for feedback channels. Once result is computed, it is stored at `RESULTS`
static ref STATUSES: SyncMap<FutureId, oneshot::Receiver<UrlResult>> = SyncMap::default();
// Cache storage for results
static ref RESULTS: SyncMap<FutureId, UrlResult> = SyncMap::default();
}
fn gen_unique_id() -> u64 { .. }
#[no_mangle]
pub extern "C" fn urlopen(url: *const c_char) -> FutureId {
let url: &str = /* convert url */;
let (tx, rx) = oneshot::channel();
RUNTIME.spawn(async move {
let body = reqwest::get(url).and_then(|b| b.text()).await;
tx.send(body).unwrap(); // <- this one should be handled somehow
});
let id = gen_unique_id();
STATUSES.write().unwrap().insert(id, rx);
id
}
Here, for each urlopen
request oneshot::channel
is being created, which delays an execution result. So it is possible to check whether it is finished or not:
#[no_mangle]
pub extern "C" fn is_finished(req_id: u64) -> bool {
// first check in cache
if RESULTS.read().unwrap().contains_key(&req_id) {
true
} else {
let mut res = RESULTS.write().unwrap();
let mut statuses = STATUSES.write().unwrap();
// if nothing in cache, check the feedback channel
if let Some(rx) = statuses.get_mut(&req_id) {
let val = match rx.try_recv() {
Ok(val) => val,
Err(_) => {
// handle error somehow here
return true;
}
};
// and cache the result, if available
res.insert(req_id, val);
true
} else {
// Unknown request id
true
}
}
}
Then the fetching result is fairly trivial:
#[no_mangle]
pub extern "C" fn fetch_result(req_id: u64) -> *const c_char {
let res = RESULTS.read().unwrap();
res.get(&req_id)
// there `ok()` should probably be handled in some better way
.and_then(|val| val.as_ref().ok())
.map(|val| val.as_ptr())
.unwrap_or(std::ptr::null()) as *const _
}
Playground link.
Keep in mind, the solution above have its advantages:
- result is cached and can be fetched multiple times;
- API is (hopefully) thread-safe;
- read and write locks are separated, which might be a faster solution than mutex;
and significant disadvantages as well:
RESULTS
grows indefinetely and never cleared;
- thread-safe make thing a little bit complicated, so might be unneeded and
thread_local!
can be used for globals instead of locks;
- lack of proper error-handling;
- RwLock is used, which sometimes might behave worse than some other primitives;
STATUSES
at is_finished
acquire write access, though might be better have a read access first;