//! Exposed C APIs for arti-rpc-client-core. //! //! See top-level documentation in header file for C conventions that affect the safety of these functions. //! (These include things like "all input pointers must be valid" and so on.) pub mod err; mod util; use err::{ArtiRpcError, InvalidInput}; use std::ffi::{c_char, c_int, c_void}; use std::sync::Mutex; use util::{ OptOutPtrExt as _, OptOutValExt, OutBoxedPtr, OutSocketOwned, OutVal, ffi_body_raw, ffi_body_with_err, }; #[cfg(not(windows))] use std::os::fd::{AsRawFd, BorrowedFd}; #[cfg(windows)] use std::os::windows::io::{AsRawSocket, BorrowedSocket}; use crate::{ ObjectId, RpcConnBuilder, RpcPoll, conn::{AnyResponse, RequestHandle}, util::Utf8CString, }; /// A status code returned by an Arti RPC function. /// /// On success, a function will return `ARTI_SUCCESS (0)`. /// On failure, a function will return some other status code. pub type ArtiRpcStatus = u32; /// An open connection to Arti over an a RPC protocol. /// /// This is a thread-safe type: you may safely use it from multiple threads at once. /// /// Once you are no longer going to use this connection at all, you must free /// it with [`arti_rpc_conn_free`] pub type ArtiRpcConn = crate::RpcConn; /// A builder object used to configure and construct /// a connection to Arti over the RPC protocol. /// /// This is a thread-safe type: you may safely use it from multiple threads at once. /// /// Once you are done with this object, you must free it with [`arti_rpc_conn_builder_free`]. pub struct ArtiRpcConnBuilder(Mutex); /// An object used to poll a nonblocking RPC connection for responses. /// /// `ArtiRpcPoll` is used to integrate an Arti RPC connection with a polling-based /// event loop. See [`arti_rpc_conn_builder_connect_polling`] for more information. // // Note: we add a mutex to ArtiRpcPoll because we do not trust the user to keep its // use to a single thread. pub struct ArtiRpcPoll(Mutex); /// An owned string, returned by this library. /// /// This string must be released with `arti_rpc_str_free`. /// You can inspect it with `arti_rpc_str_get`, but you may not modify it. /// The string is guaranteed to be UTF-8 and NUL-terminated. pub type ArtiRpcStr = Utf8CString; /// A handle to an in-progress RPC request. /// /// This handle must eventually be freed with `arti_rpc_handle_free`. /// /// You can wait for the next message with `arti_rpc_handle_wait`. pub type ArtiRpcHandle = RequestHandle; /// The type of a message returned by an RPC request. pub type ArtiRpcResponseType = c_int; /// The type of an entry prepended to a connect point search path. pub type ArtiRpcBuilderEntryType = c_int; /// The type of a data stream socket. /// (This is always `int` on Unix-like platforms, /// and SOCKET on Windows.) // // NOTE: We declare this as a separate type so that we can give it a default. #[repr(transparent)] pub struct ArtiRpcRawSocket( #[cfg(windows)] std::os::windows::raw::SOCKET, #[cfg(not(windows))] c_int, ); impl Default for ArtiRpcRawSocket { fn default() -> Self { #[cfg(windows)] { Self(!0) } #[cfg(not(windows))] { Self(-1) } } } #[cfg(not(windows))] impl<'a> From> for ArtiRpcRawSocket { fn from(value: BorrowedFd<'a>) -> Self { Self(value.as_raw_fd()) } } #[cfg(windows)] impl<'a> From> for ArtiRpcRawSocket { fn from(value: BorrowedSocket<'a>) -> Self { Self(value.as_raw_socket()) } } /// User-provided information used to implement [`crate::EventLoop`]. /// /// This type is crate-internal; in the API, we instead take its members as arguments. /// /// See [`crate::EventLoop`] for semantics, and [`arti_rpc_conn_builder_connect_polling`] /// for semantics. struct UserEventLoop { /// A function to invoke with `callback_data_ptr` when the connection starts wanting to write. /// Returns 0 or an errno. start_writing_callback: unsafe extern "C" fn(*mut c_void) -> c_int, /// A function to invoke with `callback_data_ptr` when the connection stop wanting to write. /// Returns 0 or an errno. stop_writing_callback: unsafe extern "C" fn(*mut c_void) -> c_int, /// An argument to pass to one of the callbacks in this struct. callback_data_ptr: *mut c_void, } impl crate::EventLoop for UserEventLoop { fn stop_writing(&mut self) -> std::io::Result<()> { // SAFETY: the safety requirements for this function are documented in // `arti_rpc_conn_builder_connect_polling`. let r = unsafe { (self.stop_writing_callback)(self.callback_data_ptr) }; if r == 0 { Ok(()) } else { Err(std::io::Error::from_raw_os_error(r)) } } fn start_writing(&mut self) -> std::io::Result<()> { // SAFETY: the safety requirements for this function are documented in // `arti_rpc_conn_builder_connect_polling`. let r = unsafe { (self.start_writing_callback)(self.callback_data_ptr) }; if r == 0 { Ok(()) } else { Err(std::io::Error::from_raw_os_error(r)) } } } // SAFETY: We document the thread-safety requirements for UserEventLoop's members in // `arti_rpc_conn_builder_connect_polling`. unsafe impl Send for UserEventLoop {} unsafe impl Sync for UserEventLoop {} /// A user-provided tag used to distinguish responses for requests submitted to /// [`arti_rpc_conn_submit()`]. /// /// This value is two pointers wide to facilitate using it to store /// a function pointer and an argument, where appropriate. #[derive(Clone, Copy, Default)] #[repr(C)] #[allow(missing_docs, clippy::exhaustive_structs)] pub struct ArtiRpcUserTag { pub a: usize, pub b: usize, } impl From for ArtiRpcUserTag { fn from(value: crate::UserTag) -> Self { Self { a: value.0, b: value.1, } } } impl From for crate::UserTag { fn from(value: ArtiRpcUserTag) -> Self { Self(value.a, value.b) } } /// Try to create a new `ArtiRpcConnBuilder`, with default settings. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*builder_out` /// to a new `ArtiRpcConnBuilder`. /// Otherwise return some other status code, set `*builder_out` to NULL, and set /// `*error_out` (if provided) to a newly allocated error object. /// /// # Ownership /// /// The caller is responsible for making sure that `*builder_out` and `*error_out`, /// if set, are eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_new( builder_out: *mut *mut ArtiRpcConnBuilder, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let builder_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let builder = ArtiRpcConnBuilder(Mutex::new(RpcConnBuilder::new())); builder_out.write_boxed_value_if_ptr_set(builder); } ) } /// Release storage held by an `ArtiRpcConnBuilder`. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_free(builder: *mut ArtiRpcConnBuilder) { ffi_body_raw!( { let builder: Option> [in_ptr_consume_opt]; } in { drop(builder); // Safety: return value is (); trivially safe. () } ); } /// Constant to denote a literal connect point. /// /// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`]. pub const ARTI_RPC_BUILDER_ENTRY_LITERAL_CONNECT_POINT: ArtiRpcBuilderEntryType = 1; /// Constant to denote a path in which Arti configuration variables are expanded. /// /// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`]. pub const ARTI_RPC_BUILDER_ENTRY_EXPANDABLE_PATH: ArtiRpcBuilderEntryType = 2; /// Constant to denote a literal path that is not expanded. /// /// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`]. pub const ARTI_RPC_BUILDER_ENTRY_LITERAL_PATH: ArtiRpcBuilderEntryType = 3; /// Prepend a single entry to the connection point path in `builder`. /// /// This entry will be considered before any entries in `${ARTI_RPC_CONNECT_PATH}`, /// but after any entry in `${ARTI_RPC_CONNECT_PATH_OVERRIDE}`. /// /// The interpretation will depend on the value of `entry_type`. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`. /// Otherwise return some other status code, and set /// `*error_out` (if provided) to a newly allocated error object. /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, /// if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_prepend_entry( builder: *const ArtiRpcConnBuilder, entry_type: ArtiRpcBuilderEntryType, entry: *const c_char, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt]; let entry: Option<&str> [in_str_opt]; err error_out: Option>; } in { let builder = builder.ok_or(InvalidInput::NullPointer)?; let entry = entry.ok_or(InvalidInput::NullPointer)?; let mut b = builder.0.lock().expect("Poisoned lock"); match entry_type { ARTI_RPC_BUILDER_ENTRY_LITERAL_CONNECT_POINT => b.prepend_literal_entry(entry.to_string()), ARTI_RPC_BUILDER_ENTRY_EXPANDABLE_PATH => b.prepend_path(entry.into()), ARTI_RPC_BUILDER_ENTRY_LITERAL_PATH => b.prepend_literal_path(entry.into()), _ => return Err(InvalidInput::InvalidConstValue.into()), } } ) } /// Instruct `builder` to prefer connection points that grant superuser permission. /// /// If no such connect points are found, /// and `required` is true, /// then the connection will fail with an error. /// On success, return `ARTI_RPC_STATUS_SUCCESS`. /// Otherwise return some other status code, and set /// `*error_out` (if provided) to a newly allocated error object. /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, /// if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_prefer_superuser_permission( builder: *const ArtiRpcConnBuilder, required: c_int, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt]; err error_out: Option>; } in { let builder = builder.ok_or(InvalidInput::NullPointer)?; let mut b = builder.0.lock().expect("Poisoned lock"); b.prefer_superuser_permission(required != 0); } ) } /// Use `builder` to open a new RPC connection to Arti. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`, /// and set `conn_out` to a new ArtiRpcConn. /// Otherwise return some other status code, set *conn_out to NULL, and set /// `*error_out` (if provided) to a newly allocated error object. /// /// # Ownership /// /// The caller is responsible for making sure that `*rpc_conn_out` and `*error_out`, /// if set, are eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_connect( builder: *const ArtiRpcConnBuilder, rpc_conn_out: *mut *mut ArtiRpcConn, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt]; let rpc_conn_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let builder = builder.ok_or(InvalidInput::NullPointer)?; let b = builder.0.lock().expect("Poisoned lock"); let conn = b.connect()?; rpc_conn_out.write_boxed_value_if_ptr_set(conn); } ) } /// Use `builder` to open a new RPC connection to Arti, /// with support to integrate with an event-driven IO loop. /// /// If you are not integrating with poll() or select()-style loop, /// you do not need to use this function. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`, /// set `rpc_conn_out` to a new ArtiRpcConn, /// and set `poll_out` to a new ArtiRpcPoll. /// Otherwise return some other status code, set *conn_out and *poll_out to NULL, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// # Callbacks, data, and requirements. /// /// The user code must provide an event loop /// that can monitor an underlying connection for readability and writability. /// /// The RPC library provides the user code with an OS handle to monitor. /// The user code should always monitor this handle for readability. /// It should monitor the handle for writability whenever the connection /// "wants to write". /// Whenever one of these events occurs, the user code should invoke /// `arti_rpc_poll_poll` until it indicates that it would block. /// /// The `start_writing_callback` and `start_reading_callback` functions /// must be provided. They will be invoked (respectively) whenever the connection /// starts wanting to write, or stops wanting to write. /// They should return 0 on success, and _return_ an `errno` value on failure. /// (Any `errno` value that they _set_ will be ignored.) /// They will be passed `callback_data_ptr` as an argument. /// /// If your program invokes `arti_rpc_*` from multiple threads, /// these functions must be thread-safe. /// /// There are additional requirements for these functions. /// For full information, see the documentation for [`EventLoop`](crate::EventLoop`). /// /// # Ownership /// /// The caller is responsible for making sure that `*rpc_conn_out`, /// `*rpc_poll_out`, and `*error_out`, /// if set, are eventually freed. /// /// The caller is responsible for making sure that `callback_data_ptr`, /// and the callback functions, /// live for at least as long as the returned `ArtiRpcPoll`. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_connect_polling( builder: *const ArtiRpcConnBuilder, start_writing_callback: Option c_int>, stop_writing_callback: Option c_int>, callback_data_ptr: *mut c_void, rpc_conn_out: *mut *mut ArtiRpcConn, rpc_poll_out: *mut *mut ArtiRpcPoll, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt]; let rpc_conn_out: Option> [out_ptr_opt]; let rpc_poll_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let builder = builder.ok_or(InvalidInput::NullPointer)?; let start_writing_callback = start_writing_callback.ok_or(InvalidInput::NullPointer)?; let stop_writing_callback = stop_writing_callback.ok_or(InvalidInput::NullPointer)?; let event_loop = Box::new(UserEventLoop { start_writing_callback, stop_writing_callback, callback_data_ptr, }); let b = builder.0.lock().expect("Poisoned lock"); let (conn, poll) = b.connect_polling(event_loop)?; let poll = ArtiRpcPoll(Mutex::new(poll)); rpc_conn_out.write_boxed_value_if_ptr_set(conn); rpc_poll_out.write_boxed_value_if_ptr_set(poll); } ) } /// Given a pointer to an RPC connection, return the object ID for its negotiated session. /// /// (The session was negotiated as part of establishing the connection. /// Its object ID is necessary to invoke most other functionality on Arti.) /// /// The caller should be prepared for a possible NULL return, in case somehow /// no session was negotiated. /// /// # Ownership /// /// The resulting string is a reference to part of the `ArtiRpcConn`. /// It lives for no longer than the underlying `ArtiRpcConn` object. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_get_session_id( rpc_conn: *const ArtiRpcConn, ) -> *const c_char { ffi_body_raw! { { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; } in { rpc_conn.and_then(crate::RpcConn::session) .map(|s| s.as_ptr()) .unwrap_or(std::ptr::null()) // Safety: returned pointer is null, or semantically borrowed from `rpc_conn`. // It is only null if `rpc_conn` was null or its session was null. // The caller is not allowed to modify it. } } } /// Run an RPC request over `rpc_conn` and wait for a successful response. /// /// The message `msg` should be a valid RPC request in JSON format. /// If you omit its `id` field, one will be generated: this is typically the best way to use this function. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*response_out` to a newly allocated string /// containing the JSON response to your request (including `id` and `response` fields). /// /// Otherwise return some other status code, set `*response_out` to NULL, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// (If response_out is set to NULL, then any successful response will be ignored.) /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, if set, is eventually freed. /// /// The caller is responsible for making sure that `*response_out`, if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_execute( rpc_conn: *const ArtiRpcConn, msg: *const c_char, response_out: *mut *mut ArtiRpcStr, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let msg: Option<&str> [in_str_opt]; let response_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let msg = msg.ok_or(InvalidInput::NullPointer)?; let success = rpc_conn.execute(msg)??; response_out.write_boxed_value_if_ptr_set(Utf8CString::from(success)); } ) } /// Send an RPC request over `rpc_conn`, and return a handle that can wait for a successful response. /// /// The message `msg` should be a valid RPC request in JSON format. /// If you omit its `id` field, one will be generated: this is typically the best way to use this function. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*handle_out` to a newly allocated `ArtiRpcHandle`. /// /// Otherwise return some other status code, set `*handle_out` to NULL, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// (If `handle_out` is set to NULL, the request will not be sent, and an error will be returned.) /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, if set, is eventually freed. /// /// The caller is responsible for making sure that `*handle_out`, if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_execute_with_handle( rpc_conn: *const ArtiRpcConn, msg: *const c_char, handle_out: *mut *mut ArtiRpcHandle, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let msg: Option<&str> [in_str_opt]; let handle_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let msg = msg.ok_or(InvalidInput::NullPointer)?; let handle_out = handle_out.ok_or(InvalidInput::NullPointer)?; let handle = rpc_conn.execute_with_handle(msg)?; handle_out.write_value_boxed(handle); } ) } /// Attempt to cancel the request on `rpc_conn` with the provided `handle`. /// /// Note that cancellation _will_ fail if the handle has already been cancelled, /// or has already succeeded or failed. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`. /// /// Otherwise return some other status code, /// and set `*error_out` (if provided) to a newly allocated error object. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_cancel_handle( rpc_conn: *const ArtiRpcConn, handle: *const ArtiRpcHandle, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let handle: Option<&ArtiRpcHandle> [in_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let handle = handle.ok_or(InvalidInput::NullPointer)?; let id = handle.id(); rpc_conn.cancel(id)?; } ) } /// A constant indicating that a message is a final result. /// /// After a result has been received, a handle will not return any more responses, /// and should be freed. pub const ARTI_RPC_RESPONSE_TYPE_RESULT: ArtiRpcResponseType = 1; /// A constant indicating that a message is a non-final update. /// /// After an update has been received, the handle may return additional responses. pub const ARTI_RPC_RESPONSE_TYPE_UPDATE: ArtiRpcResponseType = 2; /// A constant indicating that a message is a final error. /// /// After an error has been received, a handle will not return any more responses, /// and should be freed. pub const ARTI_RPC_RESPONSE_TYPE_ERROR: ArtiRpcResponseType = 3; impl AnyResponse { /// Return an appropriate `ARTI_RPC_RESPONSE_TYPE_*` for this response. fn response_type(&self) -> ArtiRpcResponseType { match self { Self::Success(_) => ARTI_RPC_RESPONSE_TYPE_RESULT, Self::Update(_) => ARTI_RPC_RESPONSE_TYPE_UPDATE, Self::Error(_) => ARTI_RPC_RESPONSE_TYPE_ERROR, } } } /// Wait until some response arrives on an arti_rpc_handle, or until an error occurs. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`; set `*response_out`, if present, to a /// newly allocated string, and set `*response_type_out`, if present, to the type of the response. /// (The type will be `ARTI_RPC_RESPONSE_TYPE_RESULT` if the response is a final result, /// or `ARTI_RPC_RESPONSE_TYPE_ERROR` if the response is a final error, /// or `ARTI_RPC_RESPONSE_TYPE_UPDATE` if the response is a non-final update.) /// /// Otherwise return some other status code, set `*response_out` to NULL, /// set `*response_type_out` to zero, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// Note that receiving an error reply from Arti is _not_ treated as an error in this function. /// That is to say, if Arti sends back an error, this function will return `ARTI_SUCCESS`, /// and deliver the error from Arti in `*response_out`, setting `*response_type_out` to /// `ARTI_RPC_RESPONSE_TYPE_ERROR`. /// /// It is safe to call this function on the same handle from multiple threads at once. /// If you do, each response will be sent to exactly one thread. /// It is unspecified which thread will receive which response or which error. /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, if set, is eventually freed. /// /// The caller is responsible for making sure that `*response_out`, if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_handle_wait( handle: *const ArtiRpcHandle, response_out: *mut *mut ArtiRpcStr, response_type_out: *mut ArtiRpcResponseType, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err! { { let handle: Option<&ArtiRpcHandle> [in_ptr_opt]; let response_out: Option> [out_ptr_opt]; let response_type_out: Option> [out_val_opt]; err error_out: Option>; } in { let handle = handle.ok_or(InvalidInput::NullPointer)?; let response = handle.wait_with_updates()?; let rtype = response.response_type(); response_type_out.write_value_if_ptr_set(rtype); response_out.write_boxed_value_if_ptr_set(response.into_string()); } } } /// Release storage held by an `ArtiRpcHandle`. /// /// NOTE: This does not cancel the underlying request if it is still running. /// To cancel a request, use `arti_rpc_conn_cancel_handle`. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_handle_free(handle: *mut ArtiRpcHandle) { ffi_body_raw!( { let handle: Option> [in_ptr_consume_opt]; } in { drop(handle); // Safety: Return value is (); trivially safe. () } ); } /// Submit an RPC request to `rpc_conn`, but do not wait for a response. /// /// The message `msg` should be a valid RPC request in JSON format. /// If you omit its `id` field, one will be generated: /// this is typically the best way to use this function. /// /// The `tag` value should be a value to identify this request; /// it will be returned later along with any responses to this request. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`. /// /// Otherwise return some other status code, set `*response_out` to NULL, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// After calling this function, the caller must later make sure /// that [`arti_rpc_conn_wait()`] is called on the connection to wait for responses /// to _any_ submitted request. /// (If the connection was crated with [`arti_rpc_conn_builder_connect_polling`], /// the user must call [`arti_rpc_poll_poll()`] instead.) /// /// (If nobody is running [`arti_rpc_conn_wait()`] or [`arti_rpc_poll_poll()`], /// then responses will never be handled, /// and can potentially fill up memory.) /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, if set, is eventually freed. /// /// # Thread safety /// /// It is safe to call this function from multiple threads at once; /// it is not specified which thread will receive notifications for which request. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_submit( rpc_conn: *const ArtiRpcConn, msg: *const c_char, tag: *const ArtiRpcUserTag, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let msg: Option<&str> [in_str_opt]; let tag: Option<&ArtiRpcUserTag> [in_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let msg = msg.ok_or(InvalidInput::NullPointer)?; let tag = tag.ok_or(InvalidInput::NullPointer)?; let () = rpc_conn.submit((*tag).into(), msg)?; } ) } /// Wait for responses to arrive for requests sent via [`arti_rpc_conn_submit`]. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`; /// set `*tag_out`, if present, to the `ArtiRpcUserTag` originally provided with the request; /// set `*response_out`, if present, to a newly allocated string; /// and set `*response_type_out`, if present, to the type of the response. /// (The type will be `ARTI_RPC_RESPONSE_TYPE_RESULT` if the response is a final result, /// or `ARTI_RPC_RESPONSE_TYPE_ERROR` if the response is a final error, /// or `ARTI_RPC_RESPONSE_TYPE_UPDATE` if the response is a non-final update.) /// /// Otherwise return some other status code, set `*response_out` to NULL, /// set `*response_type_out` and `*tag_out` to zero, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// Note that receiving an error reply from Arti is _not_ treated as an error in this function. /// That is to say, if Arti sends back an error, this function will return `ARTI_SUCCESS`, /// and deliver the error from Arti in `*response_out`, setting `*response_type_out` to /// `ARTI_RPC_RESPONSE_TYPE_ERROR`. /// /// # Thread safety /// /// It is safe to call this function from multiple threads at once; /// it is not specified which thread will receive notifications for which request. /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, if set, is eventually freed. /// /// The caller is responsible for making sure that `*response_out`, if set, /// is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_wait( rpc_conn: *const ArtiRpcConn, tag_out: *mut ArtiRpcUserTag, response_out: *mut *mut ArtiRpcStr, response_type_out: *mut ArtiRpcResponseType, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let tag_out: Option> [out_val_opt]; let response_out: Option> [out_ptr_opt]; let response_type_out: Option> [out_val_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let (tag, response) = rpc_conn.wait()?; tag_out.write_value_if_ptr_set(tag.into()); let rtype = response.response_type(); response_type_out.write_value_if_ptr_set(rtype); response_out.write_boxed_value_if_ptr_set(response.into_string()); } ) } /// Handle IO events for the associated RPC connection, without blocking. /// /// This method requires that the connection was created with /// [`arti_rpc_conn_builder_connect_polling()`]. /// It reads and writes data from the RPC server, until either: /// /// * A response is available to a request created with [`arti_rpc_conn_submit`]. /// In this case, /// `*tag_out`, if present, is set to the `ArtiRpcUserTag` originally provided with the request; /// `*response_out`, if present, is set to a newly allocated string; /// `*response_type_out`, if present, is set to the type of the response. /// The `*would_block_out` flag, if present, is set to 0. /// Other output pointers, if present, are set to NULL or 0. /// /// * No further progress can be made without blocking. /// In this case, /// `*would_block_out` is set to 1. /// Other output pointers, if present, are set to NULL or 0. /// /// * An error occurs. /// (This does not include receiving an error response from the RPC server.) /// In this case, `*error_out`, if present, is set to that error. /// Other output pointers, if present, are set to NULL or 0. /// /// Returns `ARTI_RPC_SUCCESS` in the first two cases, /// and an error code on failure. /// /// # Thread safety /// /// While it is not unsafe to call this function at once, /// it is generally pointless: /// only one thread can make progress at a time. /// /// # Ownership /// /// The caller is responsible for making sure /// that `*tag_out`, *response_out`, and `*error_out`, /// if set, are eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_poll_poll( rpc_poll: *const ArtiRpcPoll, tag_out: *mut ArtiRpcUserTag, response_out: *mut *mut ArtiRpcStr, response_type_out: *mut ArtiRpcResponseType, would_block_out: *mut c_int, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!({ let rpc_poll: Option<&ArtiRpcPoll> [in_ptr_opt]; let tag_out: Option> [out_val_opt]; let response_out: Option> [out_ptr_opt]; let response_type_out: Option> [out_val_opt]; let would_block_out: Option> [out_val_opt]; err error_out: Option>; } in { let rpc_poll = rpc_poll.ok_or(InvalidInput::NullPointer)?; let (tag, response) = match rpc_poll.0.lock().expect("Lock poisoned").poll()? { Ok(v) => v, Err(crate::WouldBlock) => { would_block_out.write_value_if_ptr_set(1); return Ok(()); } }; tag_out.write_value_if_ptr_set(tag.into()); let rtype = response.response_type(); response_type_out.write_value_if_ptr_set(rtype); response_out.write_boxed_value_if_ptr_set(response.into_string()); }) } /// Return true if `rpc_poll` wants to write, /// and false otherwise. /// /// See [`arti_rpc_conn_builder_connect_polling`] for more information. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_poll_wants_to_write(rpc_poll: *const ArtiRpcPoll) -> c_int { ffi_body_raw!({ let rpc_poll: Option<&ArtiRpcPoll> [in_ptr_opt]; } in { let Some(rpc_poll) = rpc_poll else { return 0; }; let wants_to_write: bool = rpc_poll.0.lock() .expect("Lock poisoned") .wants_to_write(); // Safety: return type is c_int; trivially safe. c_int::from(wants_to_write) }) } /// Return the raw OS socket associated with the provided `ArtiRpcPoll`. /// /// This function returns a SOCKET on windows, and a file descriptor elsewhere. /// /// On failure, returns INVALID_SOCKET on windows, and -1 elsewhere. /// /// # Ownership /// /// The returned socket is owned by the `ArtiRpcPoll`. /// The caller must not close it, read from it, or write to it. /// It should _only_ be polled for readiness events. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_poll_get_socket( rpc_poll: *const ArtiRpcPoll, ) -> ArtiRpcRawSocket { ffi_body_raw!({ let rpc_poll: Option<&ArtiRpcPoll> [in_ptr_opt]; } in { let Some(rpc_poll) = rpc_poll else { return ArtiRpcRawSocket::default(); }; let raw_socket: Result = { let rpc_poll = rpc_poll.0.lock().expect("Lock poisoned"); #[cfg(windows)] { rpc_poll.try_as_socket().map(ArtiRpcRawSocket::from) } #[cfg(not(windows))] { rpc_poll.try_as_fd().map(ArtiRpcRawSocket::from) } }; // Safety: return type is trivially safe. raw_socket.unwrap_or_default() }) } /// Free the provided `ArtiRpcPoll`. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_poll_free(rpc_poll: *mut ArtiRpcPoll) { ffi_body_raw!( { let rpc_poll: Option> [in_ptr_consume_opt]; } in { drop(rpc_poll); // Safety: Return value is (); trivially safe. () } ); } /// Free a string returned by the Arti RPC API. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_str_free(string: *mut ArtiRpcStr) { ffi_body_raw!( { let string: Option> [in_ptr_consume_opt]; } in { drop(string); // Safety: Return value is (); trivially safe. () } ); } /// Return a const pointer to the underlying nul-terminated string from an `ArtiRpcStr`. /// /// The resulting string is guaranteed to be valid UTF-8. /// /// (Returns NULL if the input is NULL.) /// /// # Correctness requirements /// /// The resulting string pointer is valid only for as long as the input `string` is not freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_str_get(string: *const ArtiRpcStr) -> *const c_char { ffi_body_raw!( { let string: Option<&ArtiRpcStr> [in_ptr_opt]; } in { // Safety: returned pointer is null, or semantically borrowed from `string`. // It is only null if `string` was null. // The caller is not allowed to modify it. match string { Some(s) => s.as_ptr(), None => std::ptr::null(), } } ) } /// Close and free an open Arti RPC connection. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_free(rpc_conn: *mut ArtiRpcConn) { ffi_body_raw!( { let rpc_conn: Option> [in_ptr_consume_opt]; } in { drop(rpc_conn); // Safety: Return value is (); trivially safe. () } ); } /// Try to open an anonymized data stream over Arti. /// /// Use the proxy information associated with `rpc_conn` to make the stream, /// and store the resulting fd (or `SOCKET` on Windows) into `*socket_out`. /// /// The stream will target the address `hostname`:`port`. /// /// If `on_object` is provided, it is an `ObjectId` for client-like object /// (such as a Session or a Client) /// that should be used to make the stream. /// /// The resulting stream will be configured /// not to share a circuit with any other stream /// having a different `isolation`. /// (If your application doesn't care about isolating its streams from one another, /// it is acceptable to leave `isolation` as an empty string.) /// /// If `stream_id_out` is provided, /// the resulting stream will have an identifier within the RPC system, /// so that you can run other RPC commands on it. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`. /// Otherwise return some other status code, set `*socket_out` to -1 /// (or `INVALID_SOCKET` on Windows), /// and set `*error_out` (if provided) to a newly allocated error object. /// /// # Caveats /// /// When possible, use a hostname rather than an IP address. /// If you *must* use an IP address, make sure that you have not gotten it /// by a non-anonymous DNS lookup. /// (Calling `gethostname()` or `getaddrinfo()` directly /// would lose anonymity: they inform the user's DNS server, /// and possibly many other parties, about the target address /// you are trying to visit.) /// /// The resulting socket will actually be a TCP connection to Arti, /// not directly to your destination. /// Therefore, passing it to functions like `getpeername()` /// may give unexpected results. /// /// If `stream_id_out` is provided, /// the caller is responsible for releasing the ObjectId; /// Arti will not deallocate it even when the stream is closed. /// /// # Ownership /// /// The caller is responsible for making sure that /// `*stream_id_out` and `*error_out`, if set, /// are eventually freed. /// /// The caller is responsible for making sure that `*socket_out`, if set, /// is eventually closed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_open_stream( rpc_conn: *const ArtiRpcConn, hostname: *const c_char, port: c_int, on_object: *const c_char, isolation: *const c_char, socket_out: *mut ArtiRpcRawSocket, stream_id_out: *mut *mut ArtiRpcStr, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err! { { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let on_object: Option<&str> [in_str_opt]; let hostname: Option<&str> [in_str_opt]; let isolation: Option<&str> [in_str_opt]; let socket_out: Option> [out_socket_owned_opt]; let stream_id_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let hostname = hostname.ok_or(InvalidInput::NullPointer)?; let socket_out = socket_out.ok_or(InvalidInput::NullPointer)?; let isolation = isolation.ok_or(InvalidInput::NullPointer)?; let port: u16 = port.try_into().map_err(|_| InvalidInput::BadPort)?; if port == 0 { return Err(InvalidInput::BadPort.into()); } let on_object = on_object.map(|o| ObjectId::try_from(o.to_owned())) .transpose() .expect("C string somehow contained NUL."); let stream = match stream_id_out { Some(stream_id_out) => { let (stream_id, stream) = rpc_conn.open_stream_as_object( on_object.as_ref(), (hostname, port), isolation)?; stream_id_out.write_value_boxed(stream_id.into()); stream } None => { rpc_conn.open_stream(on_object.as_ref(), (hostname, port), isolation)? } }; // We call this last so that the stream will definitely be converted to an fd, or // dropped. socket_out.write_socket(stream); } } }