Skip to content

Instantly share code, notes, and snippets.

@fasterthanlime
Created November 30, 2024 21:06
Show Gist options
  • Save fasterthanlime/0fedbf0b4a61053d4e7683eca05d99ea to your computer and use it in GitHub Desktop.
Save fasterthanlime/0fedbf0b4a61053d4e7683eca05d99ea to your computer and use it in GitHub Desktop.
//! Allows declaring traits and implementing them for structs
//! in a single go.
#[macro_export]
macro_rules! mod_entry {
($mod_name:ident => $body:block) => {
#[cfg(feature = "impl")]
#[no_mangle]
pub extern "Rust" fn awaken() -> &'static (dyn Mod) {
fn awaken() -> impl Mod {
$body
}
let m = awaken();
let m: Box<dyn Mod> = Box::new(m);
Box::leak(m)
}
#[cfg(not(feature = "impl"))]
pub fn load() -> &'static (dyn Mod) {
extern "C" {
fn dlopen(filename: *const i8, flag: i32) -> *mut std::ffi::c_void;
fn dlsym(handle: *mut std::ffi::c_void, symbol: *const i8)
-> *mut std::ffi::c_void;
fn dlerror() -> *mut i8;
}
static MOD: std::sync::LazyLock<&'static (dyn Mod)> =
::std::sync::LazyLock::new(|| {
let exe_path = std::env::current_exe().unwrap();
let exe_folder = exe_path.parent().unwrap();
let libmod_path =
exe_folder.join(format!("libmod_{}.so", stringify!($mod_name)));
unsafe {
let path_str = std::ffi::CString::new(libmod_path.to_str().unwrap())
.expect("Invalid path");
let handle = dlopen(path_str.as_ptr(), 1); // RTLD_LAZY = 1
if handle.is_null() {
let err = dlerror();
panic!(
"Failed to load module: {}",
std::ffi::CStr::from_ptr(err).to_string_lossy()
);
}
let symbol_name = std::ffi::CString::new("awaken").unwrap();
let awaken_ptr = dlsym(handle, symbol_name.as_ptr());
if awaken_ptr.is_null() {
let err = dlerror();
panic!(
"Failed to load awaken symbol: {}",
std::ffi::CStr::from_ptr(err).to_string_lossy()
);
}
let awaken: unsafe extern "Rust" fn() -> &'static (dyn Mod) =
std::mem::transmute(awaken_ptr);
awaken()
}
});
*MOD
}
};
}
#[macro_export]
macro_rules! declare_trait_and_impl {
(impl $trait_name:ident for $struct_name:ident {
$(($($signature:tt)*) $body:block)*
}) => {
pub trait $trait_name: Send + Sync + 'static {
// method declarations go here
$(
$crate::function_header!($($signature)*);
)*
}
impl dyn $trait_name {
#[doc(hidden)]
fn _assert_dyn_compatible(self: Box<Self>) {}
}
#[cfg(feature = "impl")]
impl $trait_name for $struct_name {
// method implementations go here
$($($signature)* $body)*
}
};
}
#[macro_export]
macro_rules! function_header {
(fn $name:ident(mut $($rest:tt)*) -> $ret:ty) => {
fn $name($($rest)*) -> $ret;
};
(fn $name:ident($($rest:tt)*) -> $ret:ty) => {
fn $name($($rest)*) -> $ret;
};
}
use bytes::Bytes;
use conflux2::declare_trait_and_impl;
use futures_core::{future::BoxFuture, stream::BoxStream};
use std::collections::HashMap;
#[derive(Debug)]
pub struct StatusCode(pub u16);
impl std::fmt::Display for StatusCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug)]
pub enum Error {
/// Any other error
Any(String),
/// JSON parsing error
Json(String),
/// HTTP error
Non200Status {
status: StatusCode,
response: String,
},
}
#[derive(Clone, Copy, Debug)]
pub enum Method {
GET,
POST,
PUT,
DELETE,
HEAD,
}
#[derive(Clone)]
pub struct ClientOpts {
pub resolve_to_addrs: HashMap<String, Vec<std::net::SocketAddr>>,
pub follow_redirects: bool,
}
#[cfg(feature = "impl")]
struct ModImpl {}
declare_trait_and_impl! {
impl Mod for ModImpl {
(fn client(&self) -> Box<dyn Client>) {
Box::new(ClientImpl::new(None))
}
(fn client_with_opts(&self, opts: ClientOpts) -> Box<dyn Client>) {
Box::new(ClientImpl::new(Some(opts)))
}
}
}
conflux2::mod_entry!(httpclient2 => { ModImpl {} });
#[cfg(feature = "impl")]
struct ClientImpl {
client: reqwest::Client,
}
#[cfg(feature = "impl")]
impl ClientImpl {
fn new(opts: Option<ClientOpts>) -> Self {
let mut builder = reqwest::Client::builder();
if let Some(opts) = opts {
for (host, addrs) in opts.resolve_to_addrs {
builder = builder.resolve_to_addrs(&host, &addrs);
}
if opts.follow_redirects {
builder = builder.redirect(reqwest::redirect::Policy::limited(10));
} else {
builder = builder.redirect(reqwest::redirect::Policy::none());
}
}
Self {
client: builder.build().unwrap(),
}
}
}
declare_trait_and_impl! {
impl Client for ClientImpl {
(fn request(&self, method: Method, url: &str) -> Box<dyn RequestBuilder>) {
Box::new(RequestBuilderImpl {
client: self.client.clone(),
method,
url: url.to_string(),
headers: Default::default(),
body: None,
form: None,
auth: None,
})
}
}
}
#[cfg(feature = "impl")]
struct RequestBuilderImpl {
client: reqwest::Client,
method: Method,
url: String,
headers: Vec<(String, String)>,
body: Option<Bytes>,
form: Option<String>,
auth: Option<(String, Option<String>)>,
}
declare_trait_and_impl! {
impl RequestBuilder for RequestBuilderImpl {
(fn body(mut self: Box<Self>, body: Bytes) -> Box<dyn RequestBuilder>) {
self.body = Some(body);
self
}
(fn form(mut self: Box<Self>, form: String) -> Box<dyn RequestBuilder>) {
self.form = Some(form);
self.headers.push(("content-type".to_string(), "application/x-www-form-urlencoded".to_string()));
self
}
(fn header(mut self: Box<Self>, key: &str, value: &str) -> Box<dyn RequestBuilder>) {
self.headers.push((key.to_string(), value.to_string()));
self
}
(fn basic_auth(
mut self: Box<Self>,
username: &str,
password: Option<&str>,
) -> Box<dyn RequestBuilder>) {
self.auth = Some((username.to_string(), password.map(String::from)));
self
}
(fn bearer_auth(mut self: Box<Self>, token: &str) -> Box<dyn RequestBuilder>) {
self.auth = Some((token.to_string(), None));
self
}
(fn send(self: Box<Self>) -> BoxFuture<'static, Result<Box<dyn Response>, Box<dyn std::error::Error>>>) {
let method = self.method;
let url = self.url.clone();
let headers = self.headers.clone();
let body = self.body.clone();
let form = self.form.clone();
let auth = self.auth.clone();
Box::pin(async move {
let mut request = self.client.request(match method {
Method::GET => reqwest::Method::GET,
Method::POST => reqwest::Method::POST,
Method::PUT => reqwest::Method::PUT,
Method::DELETE => reqwest::Method::DELETE,
Method::HEAD => reqwest::Method::HEAD,
}, &url);
for (key, value) in headers {
request = request.header(&key, &value);
}
if let Some(body) = body {
request = request.body(body);
}
if let Some(form) = form {
request = request.body(form);
}
if let Some((username, password)) = auth {
match password {
Some(password) => {
request = request.basic_auth(username, Some(password));
}
None => {
request = request.bearer_auth(&username);
}
}
}
let response = request.send().await?;
Ok(Box::new(ResponseImpl::new(response)) as Box<dyn Response>)
})
}
}
}
#[cfg(feature = "impl")]
struct ResponseImpl {
response: reqwest::Response,
}
#[cfg(feature = "impl")]
impl ResponseImpl {
fn new(response: reqwest::Response) -> Self {
Self { response }
}
}
declare_trait_and_impl! {
impl Response for ResponseImpl {
(fn status(&self) -> StatusCode) {
StatusCode(self.response.status().as_u16())
}
(fn headers_only_string_safe(&self) -> HashMap<String, String>) {
let mut headers = HashMap::new();
for (key, value) in self.response.headers() {
if let Ok(v) = value.to_str() {
headers.insert(key.to_string(), v.to_string());
}
}
headers
}
(fn bytes(self: Box<Self>) -> BoxFuture<'static, Result<Bytes, Error>>) {
let response = self.response;
Box::pin(async move {
response.bytes().await.map_err(|e| Error::Any(e.to_string()))
})
}
(fn bytes_stream(self: Box<Self>) -> BoxStream<'static, Result<Bytes, Error>>) {
use futures_util::StreamExt;
Box::pin(self.response
.bytes_stream()
.map(|r| r.map_err(|e| Error::Any(e.to_string()))))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment