Skip to main content

rdma_io/
device.rs

1//! RDMA device enumeration and context.
2
3use std::ffi::CStr;
4use std::sync::Arc;
5
6use rdma_io_sys::ibverbs::*;
7use rdma_io_sys::wrapper::rdma_wrap____ibv_query_port;
8
9use crate::error::{from_ptr, from_ret};
10use crate::{Error, Result};
11
12/// A discovered RDMA device (not yet opened).
13///
14/// Obtained from [`devices()`]. The device list is freed when all `Device`
15/// values are dropped.
16#[derive(Debug)]
17pub struct Device {
18    list: Arc<DeviceList>,
19    index: usize,
20}
21
22/// Owns the `ibv_device**` list returned by `ibv_get_device_list`.
23#[derive(Debug)]
24struct DeviceList {
25    list: *mut *mut ibv_device,
26    #[allow(dead_code)]
27    count: usize,
28}
29
30// Safety: ibv_device pointers are process-wide and thread-safe.
31unsafe impl Send for DeviceList {}
32unsafe impl Sync for DeviceList {}
33
34impl Drop for DeviceList {
35    fn drop(&mut self) {
36        unsafe { ibv_free_device_list(self.list) };
37    }
38}
39
40/// Enumerate all RDMA devices on the system.
41///
42/// Returns an empty `Vec` if no devices are found. Use [`open_device`](Device::open)
43/// to obtain a [`Context`].
44pub fn devices() -> Result<Vec<Device>> {
45    let mut num_devices: i32 = 0;
46    let list = unsafe { ibv_get_device_list(&mut num_devices) };
47    if list.is_null() {
48        return Err(Error::Verbs(std::io::Error::last_os_error()));
49    }
50    let count = num_devices as usize;
51    let shared = Arc::new(DeviceList { list, count });
52    let devs = (0..count)
53        .map(|i| Device {
54            list: Arc::clone(&shared),
55            index: i,
56        })
57        .collect();
58    Ok(devs)
59}
60
61impl Device {
62    /// The kernel name of this device (e.g. `"siw0"`, `"mlx5_0"`).
63    pub fn name(&self) -> &str {
64        let dev = self.as_ptr();
65        let name = unsafe { ibv_get_device_name(dev) };
66        if name.is_null() {
67            "<unknown>"
68        } else {
69            unsafe { CStr::from_ptr(name) }
70                .to_str()
71                .unwrap_or("<invalid utf8>")
72        }
73    }
74
75    /// The node GUID of this device (network byte order).
76    pub fn guid(&self) -> u64 {
77        unsafe { ibv_get_device_guid(self.as_ptr()) }
78    }
79
80    /// The transport type of this device.
81    ///
82    /// Returns `IBV_TRANSPORT_IB` for InfiniBand/RoCE (rxe, mlx5, etc.)
83    /// or `IBV_TRANSPORT_IWARP` for iWARP (siw, cxgb4, etc.).
84    pub fn transport_type(&self) -> ibv_transport_type {
85        unsafe { (*self.as_ptr()).transport_type }
86    }
87
88    /// Returns `true` if this device uses the iWARP transport (e.g. siw).
89    pub fn is_iwarp(&self) -> bool {
90        self.transport_type() == IBV_TRANSPORT_IWARP
91    }
92
93    /// Open this device and return a [`Context`].
94    pub fn open(&self) -> Result<Context> {
95        let ctx = from_ptr(unsafe { ibv_open_device(self.as_ptr()) })?;
96        Ok(Context {
97            inner: ctx,
98            owned: true,
99        })
100    }
101
102    fn as_ptr(&self) -> *mut ibv_device {
103        unsafe { *self.list.list.add(self.index) }
104    }
105}
106
107/// An opened RDMA device context.
108///
109/// Wraps `ibv_context*`. Create via [`Device::open`].
110/// All child resources (PD, CQ, QP, …) hold an `Arc<Context>` to keep
111/// the context alive.
112pub struct Context {
113    pub(crate) inner: *mut ibv_context,
114    /// If false, we don't call ibv_close_device on drop (e.g. rdma_cm-owned).
115    owned: bool,
116}
117
118// Safety: ibv_context is thread-safe (protected by internal locking).
119unsafe impl Send for Context {}
120unsafe impl Sync for Context {}
121
122impl Drop for Context {
123    fn drop(&mut self) {
124        if self.owned {
125            let ret = unsafe { ibv_close_device(self.inner) };
126            if ret != 0 {
127                tracing::error!(
128                    "ibv_close_device failed: {}",
129                    std::io::Error::from_raw_os_error(-ret)
130                );
131            }
132        }
133    }
134}
135
136impl Context {
137    /// Wrap a raw `ibv_context` pointer.
138    ///
139    /// # Safety
140    /// The pointer must be valid. If `owned` is true, `ibv_close_device` will
141    /// be called on drop. Set `owned` to false for rdma_cm-managed contexts.
142    pub unsafe fn from_raw(ctx: *mut ibv_context, owned: bool) -> Self {
143        Self { inner: ctx, owned }
144    }
145
146    /// Query device attributes.
147    pub fn query_device(&self) -> Result<ibv_device_attr> {
148        let mut attr = ibv_device_attr::default();
149        from_ret(unsafe { ibv_query_device(self.inner, &mut attr) })?;
150        Ok(attr)
151    }
152
153    /// Query port attributes.
154    pub fn query_port(&self, port_num: u8) -> Result<ibv_port_attr> {
155        let mut attr = ibv_port_attr::default();
156        from_ret(unsafe { rdma_wrap____ibv_query_port(self.inner, port_num, &mut attr) })?;
157        Ok(attr)
158    }
159
160    /// Query a single GID entry by index.
161    pub fn query_gid(&self, port_num: u8, index: i32) -> Result<ibv_gid> {
162        let mut gid = ibv_gid::default();
163        from_ret(unsafe { ibv_query_gid(self.inner, port_num, index, &mut gid) })?;
164        Ok(gid)
165    }
166
167    /// Raw `ibv_context` pointer (for advanced/FFI use).
168    pub fn as_raw(&self) -> *mut ibv_context {
169        self.inner
170    }
171}
172
173/// Open the first available RDMA device.
174///
175/// Convenience function: equivalent to `devices()?.first().open()`.
176pub fn open_first_device() -> Result<Context> {
177    let devs = devices()?;
178    if devs.is_empty() {
179        return Err(Error::NoDevices);
180    }
181    devs[0].open()
182}
183
184/// Returns `true` if the first available RDMA device is iWARP (e.g. siw).
185///
186/// Useful for tests that need to skip features unsupported on iWARP
187/// (atomics, RDMA Write with Immediate Data, etc.).
188/// Returns `true` if **any** RDMA device is iWARP (e.g. siw).
189///
190/// Useful when binding to `0.0.0.0` where the CM may pick any device —
191/// a single iWARP device in the list means the connection could land on it.
192pub fn any_device_is_iwarp() -> bool {
193    devices()
194        .ok()
195        .map(|d| d.iter().any(|dev| dev.is_iwarp()))
196        .unwrap_or(false)
197}
198
199/// Returns `true` if the given protection domain's device supports Memory Window Type 2.
200///
201/// MW Type 2 allows per-connection scoped remote write access via
202/// `IBV_WR_BIND_MW` send WRs. Checks the device capability flags
203/// (`IBV_DEVICE_MEM_WINDOW_TYPE_2A` or `IBV_DEVICE_MEM_WINDOW_TYPE_2B`).
204///
205/// Takes a `&Arc<ProtectionDomain>` to query the correct device — the PD
206/// is bound to the connection's device context via `rdma_cm` routing.
207///
208/// **Note:** Some providers (older rxe kernels) may report the flag but fail
209/// on `ibv_alloc_mw`. The ring transport surfaces a clear error at connect
210/// time if alloc fails despite the flag.
211pub fn supports_mw_type2(pd: &Arc<crate::pd::ProtectionDomain>) -> bool {
212    let attr = match pd.context().query_device() {
213        Ok(attr) => attr,
214        Err(_) => return false,
215    };
216    let flags = attr.device_cap_flags;
217    flags
218        & (rdma_io_sys::ibverbs::IBV_DEVICE_MEM_WINDOW_TYPE_2A
219            | rdma_io_sys::ibverbs::IBV_DEVICE_MEM_WINDOW_TYPE_2B)
220        != 0
221}
222
223/// Open an RDMA device by name.
224pub fn open_device_by_name(name: &str) -> Result<Context> {
225    let devs = devices()?;
226    for d in &devs {
227        if d.name() == name {
228            return d.open();
229        }
230    }
231    Err(Error::DeviceNotFound(name.to_string()))
232}