Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,28 @@ RUN apt-get -y update && \
bundler install && \
npm install -g @anthropic-ai/claude-code

# SPDK, built from source with shared libraries so libevpl can link it via
# pkg-config (EVPL_CORE_MECH_SPDK guest mode + spdk_mem_register framework).
# nvme-cuse is disabled to avoid a libfuse3 dependency; tests run without
# hugepages via spdk_env_opts.no_huge. The sed repoints the stale DPDK
# build-tree -L path in the generated .pc files at the install prefix, since
# the source tree is deleted after install.
ARG SPDK_VERSION=v25.09
RUN apt-get -y --no-install-recommends install \
python3 python3-pip python3-pyelftools python3-setuptools \
meson pkg-config nasm patchelf autoconf automake libtool help2man && \
git clone --depth 1 --branch ${SPDK_VERSION} --recurse-submodules \
--shallow-submodules https://github.com/spdk/spdk.git /tmp/spdk && \
cd /tmp/spdk && \
./configure --prefix=/usr/local --with-shared \
--disable-tests --disable-unit-tests --disable-examples \
--disable-apps --without-nvme-cuse && \
make -j$(nproc) && \
make install && \
sed -i "s|-L/tmp/spdk/dpdk/build/lib|-L/usr/local/lib|g" \
/usr/local/lib/pkgconfig/*.pc && \
ldconfig && \
cd / && rm -rf /tmp/spdk

ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
58 changes: 58 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ option(IO_URING_ENABLED "Enable io_uring support" ON)
option(IO_URING_NVME_ENABLED "Enable the io_uring NVMe uring_cmd block backend (auto-detected when ON; set OFF to force-disable, e.g. under the clang static analyzer)" ON)
option(REQUIRE_NETNS_TESTS "Fail configure if network namespace tests cannot be enabled" OFF)
set(LIBAIO_ENABLED "" CACHE STRING "Enable libaio support (empty=autodetect, YES=required, NO=disabled)")
set(SPDK_ENABLED "" CACHE STRING "Enable SPDK support (empty=autodetect, YES=required, NO=disabled)")
option(EVPL_IOVEC_PROFILE "Enable retained-iovec stack profiling" OFF)

project(libevpl LANGUAGES C)
Expand Down Expand Up @@ -220,6 +221,55 @@ else()
message(STATUS "xlio library not found.")
endif()

# SPDK guest-mode integration (EVPL_CORE_MECH_SPDK + spdk_mem_register memory
# framework). Detected via pkg-config against SPDK shared libraries
# (./configure --with-shared); static SPDK is not supported. SPDK is Linux
# only and the mechanism composes with epoll, so both are required when found.
if(NOT SPDK_ENABLED STREQUAL "NO" AND NOT SPDK_ENABLED STREQUAL "OFF")
find_package(PkgConfig)
if(PkgConfig_FOUND)
pkg_check_modules(SPDK IMPORTED_TARGET GLOBAL
spdk_bdev spdk_sock spdk_sock_posix
spdk_thread spdk_env_dpdk spdk_util spdk_log)
endif()
if(SPDK_FOUND)
message(STATUS "SPDK found via pkg-config")
add_definitions(-DHAVE_SPDK)
set(HAVE_SPDK 1)
# --no-as-needed: DPDK driver libraries (librte_mempool_ring et al.)
# and SPDK module libraries (sock impls, bdev modules) register
# themselves via constructors and are referenced by no symbol, so the
# default --as-needed link drops them and their functionality is
# silently absent at runtime.
set(SPDK_LINK_TARGETS "-Wl,--no-as-needed" PkgConfig::SPDK)

# SPDK's shared libspdk_util leaves ISA-L symbols undefined (SPDK
# normally links ISA-L statically into applications), so link ISA-L
# explicitly when its pkg-config -- installed by SPDK's own build --
# is present.
pkg_check_modules(ISAL IMPORTED_TARGET GLOBAL libisal libisal_crypto)
if(ISAL_FOUND)
list(APPEND SPDK_LINK_TARGETS PkgConfig::ISAL)
endif()

# Test-only modules: subsystem JSON bring-up (spdk_init) and the bdev
# subsystem registration constructors (spdk_event_bdev), used by the
# SPDK bdev tests to create a malloc bdev without the app framework.
pkg_check_modules(SPDK_INIT IMPORTED_TARGET GLOBAL
spdk_init spdk_event_bdev)
if(SPDK_INIT_FOUND)
set(SPDK_TEST_LINK_TARGETS PkgConfig::SPDK_INIT)
endif()
else()
if(SPDK_ENABLED STREQUAL "YES" OR SPDK_ENABLED STREQUAL "ON")
message(FATAL_ERROR "SPDK required but not found.")
endif()
message(STATUS "SPDK not found.")
endif()
else()
message(STATUS "SPDK disabled")
endif()

# OpenSSL backs the TLS protocol. macOS ships no OpenSSL/LibreSSL headers or
# linkable stubs in the SDK, so this resolves to a Homebrew openssl@3 (found
# via the prefixes added above) -- find_package is used rather than a bare
Expand Down Expand Up @@ -274,6 +324,14 @@ if(NOT EVPL_MECHANISMS)
message(FATAL_ERROR "No supported event core mechanism found on ${CMAKE_SYSTEM_NAME}")
endif()

# The SPDK mechanism delegates its fd handling to the epoll backend. SPDK is
# deliberately NOT appended to EVPL_MECHANISMS: that list replicates generic
# tests which pump evpl from a plain pthread and cannot run in guest mode;
# SPDK coverage comes from dedicated tests under src/core/spdk/tests.
if(HAVE_SPDK AND NOT EVPL_HAVE_EPOLL)
message(FATAL_ERROR "SPDK support requires the epoll core mechanism")
endif()

message(STATUS "Event core mechanisms: ${EVPL_MECHANISMS} (default: ${EVPL_MECH})")

add_definitions(-g -Wall -Werror -Wno-unused-function)
Expand Down
2 changes: 2 additions & 0 deletions docs/api/binds.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Look up a protocol ID by name.
- `"STREAM_SOCKET_TCP"` → `EVPL_STREAM_SOCKET_TCP`
- `"DATAGRAM_SOCKET_UDP"` → `EVPL_DATAGRAM_SOCKET_UDP`
- `"STREAM_XLIO_TCP"` → `EVPL_STREAM_XLIO_TCP`
- `"STREAM_SPDK_TCP"` → `EVPL_STREAM_SPDK_TCP` (spdk_sock; requires the SPDK
core mechanism inside an SPDK host application)
- `"STREAM_RDMACM_RC"` → `EVPL_STREAM_RDMACM_RC`
- `"DATAGRAM_RDMACM_RC"` → `EVPL_DATAGRAM_RDMACM_RC`
- `"DATAGRAM_RDMACM_UD"` → `EVPL_DATAGRAM_RDMACM_UD`
Expand Down
93 changes: 84 additions & 9 deletions docs/api/block.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ libevpl's Block I/O subsystem offers:

**Use cases:** Ultra-low latency storage, maximum IOPS, dedicated storage devices

### SPDK bdev

**Description:** Access to any SPDK block device (bdev) of a host SPDK application

**Availability:** Builds with SPDK installed (`SPDK_ENABLED`); at runtime
requires `EVPL_CORE_MECH_SPDK` — the opening event loop must be an evpl thread
running as an spdk_thread inside an SPDK application that owns the bdev layer.

**Characteristics:**
- Full access to the host application's bdev stack (NVMe, malloc, RAID,
logical volumes, crypto, ...)
- Userspace polled completions delivered by the bdev I/O channel on the
owning spdk_thread
- evpl slab buffers are pre-registered with `spdk_mem_register`, so I/O is
zero-copy DMA-safe
- The URI is the bdev name registered in the host application

**Use cases:** libevpl workloads embedded in an existing SPDK application that
need to share its storage stack

## Types

### `struct evpl_block_device`
Expand All @@ -70,6 +90,9 @@ Identifies block device backend:
|----------|-------------|
| `EVPL_BLOCK_PROTOCOL_IO_URING` | Linux io_uring |
| `EVPL_BLOCK_PROTOCOL_VFIO` | VFIO-NVMe direct access |
| `EVPL_BLOCK_PROTOCOL_LIBAIO` | Linux libaio |
| `EVPL_BLOCK_PROTOCOL_IO_URING_NVME` | io_uring NVMe passthrough (uring_cmd) |
| `EVPL_BLOCK_PROTOCOL_SPDK_BDEV` | SPDK bdev (requires the SPDK core mechanism) |

### `evpl_block_callback_t`

Expand All @@ -84,50 +107,102 @@ Callback invoked when a block operation completes.

**Parameters:**
- `evpl` - Event loop
- `status` - 0 on success, negative error code on failure
- `status` - 0 on success, positive errno on failure
- `private_data` - User-provided context

### `evpl_block_open_callback_t`

```c
typedef void (*evpl_block_open_callback_t)(
struct evpl *evpl,
struct evpl_block_device *blockdev,
int status,
void *private_data);
```

Callback invoked when an asynchronous device open completes.

**Parameters:**
- `evpl` - Event loop
- `blockdev` - Opened device handle, or `NULL` on failure
- `status` - 0 on success, positive errno on failure
- `private_data` - User-provided context

## Functions

### Device Management

Device open and close are asynchronous operations that run in the context of
an event loop, like every other libevpl operation. The completion callback
always fires from a later iteration of the opening loop — never inline from
the call itself — including for failures.

The opening event loop owns the device for lifecycle purposes:

- it must outlive the device,
- backend device events (such as hot-remove) are handled on its thread, and
- `evpl_block_close_device` must be called with this same event loop.

Queues may still be opened against the device from any event loop / thread.

#### `evpl_block_open_device`

```c
struct evpl_block_device *evpl_block_open_device(
void evpl_block_open_device(
struct evpl *evpl,
enum evpl_block_protocol_id protocol,
const char *uri);
const char *uri,
evpl_block_open_callback_t callback,
void *private_data);
```

Open a block device. Each device should be opened once globally for the whole process.
Open a block device asynchronously. Each device should be opened once
globally for the whole process.

**Parameters:**
- `evpl` - Event loop that will own the device
- `protocol` - Backend protocol to use
- `uri` - Device identifier (protocol-specific)

**Returns:** Block device handle, or `NULL` on failure
- `callback` - Completion callback; receives the device handle or `NULL`
- `private_data` - User context

**URI Formats:**

**io_uring:**
**io_uring / libaio:**
- Device path: `/dev/nvme0n1`
- File path: `/tmp/testfile`

**io_uring NVMe passthrough:**
- NVMe namespace block device: `/dev/nvme0n1`

**VFIO-NVMe:**
- PCI address: `0000:01:00.0`

**SPDK bdev:**
- bdev name in the host SPDK application: `Malloc0`, `Nvme0n1`

---

#### `evpl_block_close_device`

```c
void evpl_block_close_device(struct evpl_block_device *blockdev);
void evpl_block_close_device(
struct evpl *evpl,
struct evpl_block_device *blockdev,
evpl_block_callback_t callback,
void *private_data);
```

Close a block device. All queues must be closed first.
Close a block device asynchronously. All queues must be closed first, and the
call must be made on the event loop that opened the device. The device handle
is invalid as soon as this is called; the callback (which may be `NULL`) fires
from a later loop iteration once the backend has released the device.

**Parameters:**
- `evpl` - Event loop that opened the device
- `blockdev` - Device to close
- `callback` - Completion callback, or `NULL`
- `private_data` - User context

---

Expand Down
49 changes: 40 additions & 9 deletions include/evpl/evpl_block.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,49 @@
struct evpl_block_device;
struct evpl_block_queue;

struct evpl_block_device *
evpl_block_open_device(
typedef void (*evpl_block_callback_t)(
struct evpl *evpl,
int status,
void *private_data);

/*
* Completion for evpl_block_open_device. blockdev is the opened device, or
* NULL on failure; status is 0 on success or a positive errno.
*/
typedef void (*evpl_block_open_callback_t)(
struct evpl *evpl,
struct evpl_block_device *blockdev,
int status,
void *private_data);

/*
* Open a block device asynchronously. Like all libevpl operations this runs
* in the context of an event loop: the callback fires from a later iteration
* of `evpl`'s loop (never inline from this call), including for failures.
*
* The opening evpl owns the device for lifecycle purposes: it must outlive
* the device, backend device events (e.g. hot-remove) are handled on its
* thread, and evpl_block_close_device must be called with this same evpl.
* Queues may still be opened against the device from any evpl thread.
*/
void evpl_block_open_device(
struct evpl *evpl,
enum evpl_block_protocol_id protocol,
const char *uri);
const char *uri,
evpl_block_open_callback_t callback,
void *private_data);

/*
* Close a block device asynchronously. Must be called on the evpl that
* opened the device, after all of its queues have been closed. The callback
* (which may be NULL) fires from a later loop iteration once the backend has
* released the device; blockdev is invalid as soon as this is called.
*/
void evpl_block_close_device(
struct evpl_block_device *blockdev);
struct evpl *evpl,
struct evpl_block_device *blockdev,
evpl_block_callback_t callback,
void *private_data);

uint64_t evpl_block_size(
struct evpl_block_device *blockdev);
Expand All @@ -36,11 +72,6 @@ void evpl_block_close_queue(
struct evpl *evpl,
struct evpl_block_queue *queue);

typedef void (*evpl_block_callback_t)(
struct evpl *evpl,
int status,
void *private_data);

void evpl_block_read(
struct evpl *evpl,
struct evpl_block_queue *queue,
Expand Down
29 changes: 29 additions & 0 deletions include/evpl/evpl_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ enum evpl_core_mech {
EVPL_CORE_MECH_EPOLL = 1,
EVPL_CORE_MECH_KQUEUE = 2,
EVPL_CORE_MECH_SELECT = 3,
/* Guest mode inside an SPDK application: each evpl is pumped by an
* spdk_poller on the spdk_thread that created it. Never the platform
* default; requires the host to have initialized the SPDK env and thread
* library, and evpl_create() must run on an spdk_thread. */
EVPL_CORE_MECH_SPDK = 4,
};

struct evpl_global_config *
Expand Down Expand Up @@ -122,6 +127,20 @@ void evpl_thread_config_set_wait_ms(
struct evpl_thread_config *config,
int wait_ms);

/* Thread name, used e.g. to name the spdk_thread created for an evpl_thread
* under EVPL_CORE_MECH_SPDK. Truncated to the config field size. */
void evpl_thread_config_set_name(
struct evpl_thread_config *config,
const char *name);

/* SPDK cpumask string (as accepted by spdk_cpuset_parse, e.g. "0x3" or
* "[0,1]") constraining where the host scheduler may place the spdk_thread
* created for an evpl_thread. Empty (default) lets the host decide. Only
* meaningful under EVPL_CORE_MECH_SPDK. */
void evpl_thread_config_set_spdk_cpumask(
struct evpl_thread_config *config,
const char *cpumask);

void evpl_global_config_set_slab_size(
struct evpl_global_config *config,
uint64_t size);
Expand Down Expand Up @@ -210,6 +229,16 @@ void evpl_global_config_set_libaio_enabled(
struct evpl_global_config *config,
int enabled);

void evpl_global_config_set_spdk_enabled(
struct evpl_global_config *config,
int enabled);

/* spdk_sock implementation for STREAM_SPDK_TCP ("posix", "uring", ...);
* NULL (default) selects SPDK's default implementation. */
void evpl_global_config_set_spdk_sock_impl(
struct evpl_global_config *config,
const char *impl_name);

void evpl_global_config_set_libaio_max_pending(
struct evpl_global_config *config,
unsigned int max_pending);
Expand Down
Loading
Loading