arti_rpc_client_core/
ffi.rs

1//! Exposed C APIs for arti-rpc-client-core.
2//!
3//! See top-level documentation in header file for C conventions that affect the safety of these functions.
4//! (These include things like "all input pointers must be valid" and so on.)
5
6pub mod err;
7mod util;
8
9use err::{ArtiRpcError, InvalidInput};
10use std::ffi::{c_char, c_int};
11use std::sync::Mutex;
12use util::{
13    ffi_body_raw, ffi_body_with_err, OptOutPtrExt as _, OptOutValExt, OutPtr, OutSocketOwned,
14    OutVal,
15};
16
17use crate::{
18    conn::{AnyResponse, RequestHandle},
19    util::Utf8CString,
20    ObjectId, RpcConnBuilder,
21};
22
23/// A status code returned by an Arti RPC function.
24///
25/// On success, a function will return `ARTI_SUCCESS (0)`.
26/// On failure, a function will return some other status code.
27pub type ArtiRpcStatus = u32;
28
29/// An open connection to Arti over an a RPC protocol.
30///
31/// This is a thread-safe type: you may safely use it from multiple threads at once.
32///
33/// Once you are no longer going to use this connection at all, you must free
34/// it with [`arti_rpc_conn_free`]
35pub type ArtiRpcConn = crate::RpcConn;
36
37/// A builder object used to configure and construct
38/// a connection to Arti over the RPC protocol.
39///
40/// This is a thread-safe type: you may safely use it from multiple threads at once.
41///
42/// Once you are done with this object, you must free it with [`arti_rpc_conn_builder_free`].
43pub struct ArtiRpcConnBuilder(Mutex<RpcConnBuilder>);
44
45/// An owned string, returned by this library.
46///
47/// This string must be released with `arti_rpc_str_free`.
48/// You can inspect it with `arti_rpc_str_get`, but you may not modify it.
49/// The string is guaranteed to be UTF-8 and NUL-terminated.
50pub type ArtiRpcStr = Utf8CString;
51
52/// A handle to an in-progress RPC request.
53///
54/// This handle must eventually be freed with `arti_rpc_handle_free`.
55///
56/// You can wait for the next message with `arti_rpc_handle_wait`.
57pub type ArtiRpcHandle = RequestHandle;
58
59/// The type of a message returned by an RPC request.
60pub type ArtiRpcResponseType = c_int;
61
62/// The type of an entry prepended to a connect point search path.
63pub type ArtiRpcBuilderEntryType = c_int;
64
65/// The type of a data stream socket.
66/// (This is always `int` on Unix-like platforms,
67/// and SOCKET on Windows.)
68//
69// NOTE: We declare this as a separate type so that we can give it a default.
70#[repr(transparent)]
71pub struct ArtiRpcRawSocket(
72    #[cfg(windows)] std::os::windows::raw::SOCKET,
73    #[cfg(not(windows))] c_int,
74);
75
76impl Default for ArtiRpcRawSocket {
77    fn default() -> Self {
78        #[cfg(windows)]
79        {
80            Self(!0)
81        }
82        #[cfg(not(windows))]
83        {
84            Self(-1)
85        }
86    }
87}
88
89/// Try to create a new `ArtiRpcConnBuilder`, with default settings.
90///
91/// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*builder_out`
92/// to a new `ArtiRpcConnBuilder`.
93/// Otherwise return some other status code, set `*builder_out` to NULL, and set
94/// `*error_out` (if provided) to a newly allocated error object.
95///
96/// # Ownership
97///
98/// The caller is responsible for making sure that `*builder_out` and `*error_out`,
99/// if set, are eventually freed.
100#[allow(clippy::missing_safety_doc)]
101#[no_mangle]
102pub unsafe extern "C" fn arti_rpc_conn_builder_new(
103    builder_out: *mut *mut ArtiRpcConnBuilder,
104    error_out: *mut *mut ArtiRpcError,
105) -> ArtiRpcStatus {
106    ffi_body_with_err!(
107        {
108            let builder_out: Option<OutPtr<ArtiRpcConnBuilder>> [out_ptr_opt];
109            err error_out: Option<OutPtr<ArtiRpcError>>;
110        } in {
111            let builder = ArtiRpcConnBuilder(Mutex::new(RpcConnBuilder::new()));
112            builder_out.write_boxed_value_if_ptr_set(builder);
113        }
114    )
115}
116
117/// Release storage held by an `ArtiRpcConnBuilder`.
118#[allow(clippy::missing_safety_doc)]
119#[no_mangle]
120pub unsafe extern "C" fn arti_rpc_conn_builder_free(builder: *mut ArtiRpcConnBuilder) {
121    ffi_body_raw!(
122        {
123            let builder: Option<Box<ArtiRpcConnBuilder>> [in_ptr_consume_opt];
124        } in {
125            drop(builder);
126            // Safety: return value is (); trivially safe.
127            ()
128        }
129    );
130}
131
132/// Constant to denote a literal connect point.
133///
134/// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`].
135pub const ARTI_RPC_BUILDER_ENTRY_LITERAL_CONNECT_POINT: ArtiRpcBuilderEntryType = 1;
136
137/// Constant to denote a path in which Arti configuration variables are expanded.
138///
139/// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`].
140pub const ARTI_RPC_BUILDER_ENTRY_EXPANDABLE_PATH: ArtiRpcBuilderEntryType = 2;
141
142/// Constant to denote a literal path that is not expanded.
143///
144/// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`].
145pub const ARTI_RPC_BUILDER_ENTRY_LITERAL_PATH: ArtiRpcBuilderEntryType = 3;
146
147/// Prepend a single entry to the connection point path in `builder`.
148///
149/// This entry will be considered before any entries in `${ARTI_RPC_CONNECT_PATH}`,
150/// but after any entry in `${ARTI_RPC_CONNECT_PATH_OVERRIDE}`.
151///
152/// The interpretation will depend on the value of `entry_type`.
153///
154/// On success, return `ARTI_RPC_STATUS_SUCCESS`.
155/// Otherwise return some other status code, and set
156/// `*error_out` (if provided) to a newly allocated error object.
157///
158/// # Ownership
159///
160/// The caller is responsible for making sure that `*error_out`,
161/// if set, is eventually freed.
162#[allow(clippy::missing_safety_doc)]
163#[no_mangle]
164pub unsafe extern "C" fn arti_rpc_conn_builder_prepend_entry(
165    builder: *const ArtiRpcConnBuilder,
166    entry_type: ArtiRpcBuilderEntryType,
167    entry: *const c_char,
168    error_out: *mut *mut ArtiRpcError,
169) -> ArtiRpcStatus {
170    ffi_body_with_err!(
171        {
172            let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt];
173            let entry: Option<&str> [in_str_opt];
174            err error_out: Option<OutPtr<ArtiRpcError>>;
175        } in {
176            let builder = builder.ok_or(InvalidInput::NullPointer)?;
177            let entry = entry.ok_or(InvalidInput::NullPointer)?;
178            let mut b = builder.0.lock().expect("Poisoned lock");
179            match entry_type {
180                ARTI_RPC_BUILDER_ENTRY_LITERAL_CONNECT_POINT =>
181                    b.prepend_literal_entry(entry.to_string()),
182                ARTI_RPC_BUILDER_ENTRY_EXPANDABLE_PATH =>
183                    b.prepend_path(entry.into()),
184                ARTI_RPC_BUILDER_ENTRY_LITERAL_PATH =>
185                    b.prepend_literal_path(entry.into()),
186                _ => return Err(InvalidInput::InvalidConstValue.into()),
187            }
188        }
189    )
190}
191
192/// Use `builder` to open a new RPC connection to Arti.
193///
194/// On success, return `ARTI_RPC_STATUS_SUCCESS`,
195/// and set `conn_out` to a new ArtiRpcConn.
196/// Otherwise return some other status code, set *conn_out to NULL, and set
197/// `*error_out` (if provided) to a newly allocated error object.
198///
199/// # Ownership
200///
201/// The caller is responsible for making sure that `*rpc_conn_out` and `*error_out`,
202/// if set, are eventually freed.
203#[allow(clippy::missing_safety_doc)]
204#[no_mangle]
205pub unsafe extern "C" fn arti_rpc_conn_builder_connect(
206    builder: *const ArtiRpcConnBuilder,
207    rpc_conn_out: *mut *mut ArtiRpcConn,
208    error_out: *mut *mut ArtiRpcError,
209) -> ArtiRpcStatus {
210    ffi_body_with_err!(
211        {
212            let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt];
213            let rpc_conn_out: Option<OutPtr<ArtiRpcConn>> [out_ptr_opt];
214            err error_out: Option<OutPtr<ArtiRpcError>>;
215        } in {
216            let builder = builder.ok_or(InvalidInput::NullPointer)?;
217            let b = builder.0.lock().expect("Poisoned lock");
218            let conn = b.connect()?;
219            rpc_conn_out.write_boxed_value_if_ptr_set(conn);
220        }
221    )
222}
223
224/// Given a pointer to an RPC connection, return the object ID for its negotiated session.
225///
226/// (The session was negotiated as part of establishing the connection.
227/// Its object ID is necessary to invoke most other functionality on Arti.)
228///
229/// The caller should be prepared for a possible NULL return, in case somehow
230/// no session was negotiated.
231///
232/// # Ownership
233///
234/// The resulting string is a reference to part of the `ArtiRpcConn`.
235/// It lives for no longer than the underlying `ArtiRpcConn` object.
236#[allow(clippy::missing_safety_doc)]
237#[no_mangle]
238pub unsafe extern "C" fn arti_rpc_conn_get_session_id(
239    rpc_conn: *const ArtiRpcConn,
240) -> *const c_char {
241    ffi_body_raw! {
242        {
243            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
244        } in {
245            rpc_conn.and_then(crate::RpcConn::session)
246                .map(|s| s.as_ptr())
247                .unwrap_or(std::ptr::null())
248            // Safety: returned pointer is null, or semantically borrowed from `rpc_conn`.
249            // It is only null if `rpc_conn` was null or its session was null.
250            // The caller is not allowed to modify it.
251        }
252    }
253}
254
255/// Run an RPC request over `rpc_conn` and wait for a successful response.
256///
257/// The message `msg` should be a valid RPC request in JSON format.
258/// If you omit its `id` field, one will be generated: this is typically the best way to use this function.
259///
260/// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*response_out` to a newly allocated string
261/// containing the JSON response to your request (including `id` and `response` fields).
262///
263/// Otherwise return some other status code,  set `*response_out` to NULL,
264/// and set `*error_out` (if provided) to a newly allocated error object.
265///
266/// (If response_out is set to NULL, then any successful response will be ignored.)
267///
268/// # Ownership
269///
270/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
271///
272/// The caller is responsible for making sure that `*response_out`, if set, is eventually freed.
273#[allow(clippy::missing_safety_doc)]
274#[no_mangle]
275pub unsafe extern "C" fn arti_rpc_conn_execute(
276    rpc_conn: *const ArtiRpcConn,
277    msg: *const c_char,
278    response_out: *mut *mut ArtiRpcStr,
279    error_out: *mut *mut ArtiRpcError,
280) -> ArtiRpcStatus {
281    ffi_body_with_err!(
282        {
283            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
284            let msg: Option<&str> [in_str_opt];
285            let response_out: Option<OutPtr<ArtiRpcStr>> [out_ptr_opt];
286            err error_out: Option<OutPtr<ArtiRpcError>>;
287        } in {
288            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
289            let msg = msg.ok_or(InvalidInput::NullPointer)?;
290
291            let success = rpc_conn.execute(msg)??;
292            response_out.write_boxed_value_if_ptr_set(Utf8CString::from(success));
293        }
294    )
295}
296
297/// Send an RPC request over `rpc_conn`, and return a handle that can wait for a successful response.
298///
299/// The message `msg` should be a valid RPC request in JSON format.
300/// If you omit its `id` field, one will be generated: this is typically the best way to use this function.
301///
302/// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*handle_out` to a newly allocated `ArtiRpcHandle`.
303///
304/// Otherwise return some other status code,  set `*handle_out` to NULL,
305/// and set `*error_out` (if provided) to a newly allocated error object.
306///
307/// (If `handle_out` is set to NULL, the request will not be sent, and an error will be returned.)
308///
309/// # Ownership
310///
311/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
312///
313/// The caller is responsible for making sure that `*handle_out`, if set, is eventually freed.
314#[allow(clippy::missing_safety_doc)]
315#[no_mangle]
316pub unsafe extern "C" fn arti_rpc_conn_execute_with_handle(
317    rpc_conn: *const ArtiRpcConn,
318    msg: *const c_char,
319    handle_out: *mut *mut ArtiRpcHandle,
320    error_out: *mut *mut ArtiRpcError,
321) -> ArtiRpcStatus {
322    ffi_body_with_err!(
323        {
324            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
325            let msg: Option<&str> [in_str_opt];
326            let handle_out: Option<OutPtr<ArtiRpcHandle>> [out_ptr_opt];
327            err error_out: Option<OutPtr<ArtiRpcError>>;
328        } in {
329            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
330            let msg = msg.ok_or(InvalidInput::NullPointer)?;
331            let handle_out = handle_out.ok_or(InvalidInput::NullPointer)?;
332
333            let handle = rpc_conn.execute_with_handle(msg)?;
334            handle_out.write_value_boxed(handle);
335        }
336    )
337}
338
339/// Attempt to cancel the request on `rpc_conn` with the provided `handle`.
340///
341/// Note that cancellation _will_ fail if the handle has already been cancelled,
342/// or has already succeeded or failed.
343///
344/// On success, return `ARTI_RPC_STATUS_SUCCESS`.
345///
346/// Otherwise return some other status code,
347/// and set `*error_out` (if provided) to a newly allocated error object.
348#[allow(clippy::missing_safety_doc)]
349#[no_mangle]
350pub unsafe extern "C" fn arti_rpc_conn_cancel_handle(
351    rpc_conn: *const ArtiRpcConn,
352    handle: *const ArtiRpcHandle,
353    error_out: *mut *mut ArtiRpcError,
354) -> ArtiRpcStatus {
355    ffi_body_with_err!(
356        {
357            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
358            let handle: Option<&ArtiRpcHandle> [in_ptr_opt];
359            err error_out: Option<OutPtr<ArtiRpcError>>;
360        } in {
361            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
362            let handle = handle.ok_or(InvalidInput::NullPointer)?;
363            let id = handle.id();
364            rpc_conn.cancel(id)?;
365        }
366    )
367}
368
369/// A constant indicating that a message is a final result.
370///
371/// After a result has been received, a handle will not return any more responses,
372/// and should be freed.
373pub const ARTI_RPC_RESPONSE_TYPE_RESULT: ArtiRpcResponseType = 1;
374/// A constant indicating that a message is a non-final update.
375///
376/// After an update has been received, the handle may return additional responses.
377pub const ARTI_RPC_RESPONSE_TYPE_UPDATE: ArtiRpcResponseType = 2;
378/// A constant indicating that a message is a final error.
379///
380/// After an error has been received, a handle will not return any more responses,
381/// and should be freed.
382pub const ARTI_RPC_RESPONSE_TYPE_ERROR: ArtiRpcResponseType = 3;
383
384impl AnyResponse {
385    /// Return an appropriate `ARTI_RPC_RESPONSE_TYPE_*` for this response.
386    fn response_type(&self) -> ArtiRpcResponseType {
387        match self {
388            Self::Success(_) => ARTI_RPC_RESPONSE_TYPE_RESULT,
389            Self::Update(_) => ARTI_RPC_RESPONSE_TYPE_UPDATE,
390            Self::Error(_) => ARTI_RPC_RESPONSE_TYPE_ERROR,
391        }
392    }
393}
394
395/// Wait until some response arrives on an arti_rpc_handle, or until an error occurs.
396///
397/// On success, return `ARTI_RPC_STATUS_SUCCESS`; set `*response_out`, if present, to a
398/// newly allocated string, and set `*response_type_out`, to the type of the response.
399/// (The type will be `ARTI_RPC_RESPONSE_TYPE_RESULT` if the response is a final result,
400/// or `ARTI_RPC_RESPONSE_TYPE_ERROR` if the response is a final error,
401/// or `ARTI_RPC_RESPONSE_TYPE_UPDATE` if the response is a non-final update.)
402///
403/// Otherwise return some other status code, set `*response_out` to NULL,
404/// set `*response_type_out` to zero,
405/// and set `*error_out` (if provided) to a newly allocated error object.
406///
407/// Note that receiving an error reply from Arti is _not_ treated as an error in this function.
408/// That is to say, if Arti sends back an error, this function will return `ARTI_SUCCESS`,
409/// and deliver the error from Arti in `*response_out`, setting `*response_type_out` to
410/// `ARTI_RPC_RESPONSE_TYPE_ERROR`.
411///
412/// It is safe to call this function on the same handle from multiple threads at once.
413/// If you do, each response will be sent to exactly one thread.
414/// It is unspecified which thread will receive which response or which error.
415///
416/// # Ownership
417///
418/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
419///
420/// The caller is responsible for making sure that `*response_out`, if set, is eventually freed.
421#[allow(clippy::missing_safety_doc)]
422#[no_mangle]
423pub unsafe extern "C" fn arti_rpc_handle_wait(
424    handle: *const ArtiRpcHandle,
425    response_out: *mut *mut ArtiRpcStr,
426    response_type_out: *mut ArtiRpcResponseType,
427    error_out: *mut *mut ArtiRpcError,
428) -> ArtiRpcStatus {
429    ffi_body_with_err! {
430        {
431            let handle: Option<&ArtiRpcHandle> [in_ptr_opt];
432            let response_out: Option<OutPtr<ArtiRpcStr>> [out_ptr_opt];
433            let response_type_out: Option<OutVal<ArtiRpcResponseType>> [out_val_opt];
434            err error_out: Option<OutPtr<ArtiRpcError>>;
435        } in {
436            let handle = handle.ok_or(InvalidInput::NullPointer)?;
437
438            let response = handle.wait_with_updates()?;
439
440            let rtype = response.response_type();
441            response_type_out.write_value_if_ptr_set(rtype);
442            response_out.write_boxed_value_if_ptr_set(response.into_string());
443        }
444    }
445}
446
447/// Release storage held by an `ArtiRpcHandle`.
448///
449/// NOTE: This does not cancel the underlying request if it is still running.
450/// To cancel a request, use `arti_rpc_conn_cancel_handle`.
451#[allow(clippy::missing_safety_doc)]
452#[no_mangle]
453pub unsafe extern "C" fn arti_rpc_handle_free(handle: *mut ArtiRpcHandle) {
454    ffi_body_raw!(
455        {
456            let handle: Option<Box<ArtiRpcHandle>> [in_ptr_consume_opt];
457        } in {
458            drop(handle);
459            // Safety: Return value is (); trivially safe.
460            ()
461        }
462    );
463}
464/// Free a string returned by the Arti RPC API.
465#[allow(clippy::missing_safety_doc)]
466#[no_mangle]
467pub unsafe extern "C" fn arti_rpc_str_free(string: *mut ArtiRpcStr) {
468    ffi_body_raw!(
469        {
470            let string: Option<Box<ArtiRpcStr>> [in_ptr_consume_opt];
471        } in {
472            drop(string);
473            // Safety: Return value is (); trivially safe.
474            ()
475        }
476    );
477}
478
479/// Return a const pointer to the underlying nul-terminated string from an `ArtiRpcStr`.
480///
481/// The resulting string is guaranteed to be valid UTF-8.
482///
483/// (Returns NULL if the input is NULL.)
484///
485/// # Correctness requirements
486///
487/// The resulting string pointer is valid only for as long as the input `string` is not freed.
488#[allow(clippy::missing_safety_doc)]
489#[no_mangle]
490pub unsafe extern "C" fn arti_rpc_str_get(string: *const ArtiRpcStr) -> *const c_char {
491    ffi_body_raw!(
492        {
493            let string: Option<&ArtiRpcStr> [in_ptr_opt];
494        } in {
495            // Safety: returned pointer is null, or semantically borrowed from `string`.
496            // It is only null if `string` was null.
497            // The caller is not allowed to modify it.
498            match string {
499                Some(s) => s.as_ptr(),
500                None => std::ptr::null(),
501            }
502
503        }
504    )
505}
506
507/// Close and free an open Arti RPC connection.
508#[allow(clippy::missing_safety_doc)]
509#[no_mangle]
510pub unsafe extern "C" fn arti_rpc_conn_free(rpc_conn: *mut ArtiRpcConn) {
511    ffi_body_raw!(
512        {
513            let rpc_conn: Option<Box<ArtiRpcConn>> [in_ptr_consume_opt];
514        } in {
515            drop(rpc_conn);
516            // Safety: Return value is (); trivially safe.
517            ()
518
519        }
520    );
521}
522
523/// Try to open an anonymized data stream over Arti.
524///
525/// Use the proxy information associated with `rpc_conn` to make the stream,
526/// and store the resulting fd (or `SOCKET` on Windows) into `*socket_out`.
527///
528/// The stream will target the address `hostname`:`port`.
529///
530/// If `on_object` is provided, it is an `ObjectId` for client-like object
531/// (such as a Session or a Client)
532/// that should be used to make the stream.
533///
534/// The resulting stream will be configured
535/// not to share a circuit with any other stream
536/// having a different `isolation`.
537/// (If your application doesn't care about isolating its streams from one another,
538/// it is acceptable to leave `isolation` as an empty string.)
539///
540/// If `stream_id_out` is provided,
541/// the resulting stream will have an identifier within the RPC system,
542/// so that you can run other RPC commands on it.
543///
544/// On success, return `ARTI_RPC_STATUS_SUCCESS`.
545/// Otherwise return some other status code, set `*socket_out` to -1
546/// (or `INVALID_SOCKET` on Windows),
547/// and set `*error_out` (if provided) to a newly allocated error object.
548///
549/// # Caveats
550///
551/// When possible, use a hostname rather than an IP address.
552/// If you *must* use an IP address, make sure that you have not gotten it
553/// by a non-anonymous DNS lookup.
554/// (Calling `gethostname()` or `getaddrinfo()` directly
555/// would lose anonymity: they inform the user's DNS server,
556/// and possibly many other parties, about the target address
557/// you are trying to visit.)
558///
559/// The resulting socket will actually be a TCP connection to Arti,
560/// not directly to your destination.
561/// Therefore, passing it to functions like `getpeername()`
562/// may give unexpected results.
563///
564/// If `stream_id_out` is provided,
565/// the caller is responsible for releasing the ObjectId;
566/// Arti will not deallocate it even when the stream is closed.
567///
568/// # Ownership
569///
570/// The caller is responsible for making sure that
571/// `*stream_id_out` and `*error_out`, if set,
572/// are eventually freed.
573///
574/// The caller is responsible for making sure that `*socket_out`, if set,
575/// is eventually closed.
576#[allow(clippy::missing_safety_doc)]
577#[no_mangle]
578pub unsafe extern "C" fn arti_rpc_conn_open_stream(
579    rpc_conn: *const ArtiRpcConn,
580    hostname: *const c_char,
581    port: c_int,
582    on_object: *const c_char,
583    isolation: *const c_char,
584    socket_out: *mut ArtiRpcRawSocket,
585    stream_id_out: *mut *mut ArtiRpcStr,
586    error_out: *mut *mut ArtiRpcError,
587) -> ArtiRpcStatus {
588    ffi_body_with_err! {
589        {
590            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
591            let on_object: Option<&str> [in_str_opt];
592            let hostname: Option<&str> [in_str_opt];
593            let isolation: Option<&str> [in_str_opt];
594            let socket_out: Option<OutSocketOwned<'_>> [out_socket_owned_opt];
595            let stream_id_out: Option<OutPtr<ArtiRpcStr>> [out_ptr_opt];
596            err error_out: Option<OutPtr<ArtiRpcError>>;
597        } in {
598            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
599            let hostname = hostname.ok_or(InvalidInput::NullPointer)?;
600            let socket_out = socket_out.ok_or(InvalidInput::NullPointer)?;
601            let isolation = isolation.ok_or(InvalidInput::NullPointer)?;
602
603            let port: u16 = port.try_into().map_err(|_| InvalidInput::BadPort)?;
604            if port == 0 {
605                return Err(InvalidInput::BadPort.into());
606            }
607
608            let on_object = on_object.map(|o| ObjectId::try_from(o.to_owned()))
609                .transpose()
610                .expect("C string somehow contained NUL.");
611
612            let stream = match stream_id_out {
613                Some(stream_id_out) => {
614                    let (stream_id, stream) = rpc_conn.open_stream_as_object(
615                        on_object.as_ref(),
616                        (hostname, port),
617                        isolation)?;
618                    stream_id_out.write_value_boxed(stream_id.into());
619                    stream
620                }
621                None => {
622                    rpc_conn.open_stream(on_object.as_ref(), (hostname, port), isolation)?
623                }
624            };
625
626            // We call this last so that the stream will definitely be converted to an fd, or
627            // dropped.
628            socket_out.write_socket(stream);
629        }
630    }
631}