Layer 8 · Write a bdev module

Everything outside the .c file.

A bdev module is more than its C source. To build, you need a Makefile and an entry in module/bdev/Makefile. To link, you need an entry in mk/spdk.modules.mk. To respond to JSON-RPC, you need a companion <name>_rpc.c file with SPDK_RPC_REGISTER calls. To opt in to optional dependencies, you need a flag in configure. This page covers every one of those touchpoints, and ends with a complete skeleton you can copy and start filling in.

~15 min read1 diagramprerequisites: 4.2 · 8.1
On this page
  1. The seven build touchpoints
  2. 1. The per-module Makefile
  3. 2. The parent module/bdev/Makefile
  4. 3. mk/spdk.modules.mk — the modules list
  5. 4. The configure script
  6. 5. JSON-RPC registration
  7. 6. Module documentation (optional)
  8. 7. Testing with bdevperf
  9. Edge cases: build-time footguns
  10. Scaffolding skeleton: a complete .c template

The seven build touchpoints

The full pipeline from "I have a .c file" to "spdk_bdev_create works" has seven touchpoints. The first four are required; the rest are for modules that need optional dependencies, RPCs, or docs.

STEP 01
1. module/bdev//Makefile
Compile your .c into a .so/.a
STEP 02
2. module/bdev/Makefile
Recurse into your subdir
STEP 03
3. mk/spdk.modules.mk
Add your module to BLOCKDEV_MODULES_LIST
STEP 04
4. configure
(Optional) --with-<name> flag for optional deps
STEP 05
5. JSON-RPC
Companion _rpc.c with SPDK_RPC_REGISTER
STEP 06
6. doc/
(Optional) markdown docs for users
STEP 07
7. bdevperf
Test it with the example app
flowchart TB
Src["my_bdev.c"] --> M1
Rpc["my_bdev_rpc.c"] --> M1
M1["module/bdev/my_bdev/Makefile"] --> M2
M2["module/bdev/Makefile (DIRS-y)"] --> M3
M3["mk/spdk.modules.mk (BLOCKDEV_MODULES_LIST)"] --> M4
M4["configure (--with-my-bdev flag, optional)"] --> Build
M3 --> Build
Build["spdk build → libbdev_my_bdev.so"] --> Register
Src -->|"SPDK_BDEV_MODULE_REGISTER"| Register
Rpc -->|"SPDK_RPC_REGISTER"| Register
Register["framework module list + RPC method table"] --> Ready
Ready["ready to receive bdev_my_bdev_create RPC"]
fig. 1 — how a bdev module gets built and registered · tap or scroll to zoom · ↗ for fullscreen

fig. 1   The build pipeline. The SPDK_BDEV_MODULE_REGISTER constructor in the .c file is the magic that makes the framework aware of your module; the Makefile and modules.mk entry are the magic that makes the linker include your object file.

1. The per-module Makefile

Each bdev module has a Makefile in its own directory. The passthru one is the canonical example:

Every bdev module's Makefile has the same shape. Five mandatory pieces:

  1. SPDK_ROOT_DIR := $(abspath $(CURDIR)/../../..) — walk up three directories from the module's Makefile to the SPDK repo root. The abspath is important: it lets the build work from any cwd.

  2. include $(SPDK_ROOT_DIR)/mk/spdk.common.mk — common build flags, dependency tracking, output directory setup. Required.

  3. C_SRCS = vbdev_passthru.c vbdev_passthru_rpc.c — your source files. If you have an RPC file, list it here too. If you split the module into multiple .c files, list them all.

  4. LIBNAME = bdev_passthru — the output library name. The convention is bdev_<name>. This produces libbdev_passthru.so (and .so.7.0 as a versioned symlink).

  5. include $(SPDK_ROOT_DIR)/mk/spdk.lib.mk — the rules for building a library. Required. This is what turns C_SRCS and LIBNAME into the actual .o and .so files.

Two optional pieces:

  1. SO_VER := 7 and SO_MINOR := 0 — the shared library version. Increment SO_VER when you break ABI; bump SO_MINOR for backward-compatible changes. The malloc and null modules use the same values; the convention is to keep all bdev modules on the same version.

  2. SPDK_MAP_FILE = $(SPDK_ROOT_DIR)/mk/spdk_blank.map — the symbol export list. The blank map exports everything, which is what you want for a module that gets linked into the SPDK app statically. If you're building a plugin that gets dlopen()ed, you'd use a stricter map with only the public symbols.

2. The parent module/bdev/Makefile

Two changes to add a new module:

  1. Add the module's directory to DIRS-y. Order doesn't matter functionally; the SPDK convention is alphabetical, with the small modules (delay, error, gpt) first.

  2. If the module requires an optional dependency, add it to a DIRS-$(CONFIG_XXX) line. The CONFIG_XXX variable is set by the configure script based on which --with-XXX flags were passed.

That's the whole change. spdk.subdirs.mk recurses into each directory in DIRS-y and runs the per-module Makefile.

3. mk/spdk.modules.mk — the modules list

This is the file the SPDK build system reads to know which modules to build, link, and ship in the resulting binaries. The variable name is BLOCKDEV_MODULES_LIST; each entry is a library name (matching your LIBNAME). Adding your module is one line.

Why does this list exist, separate from module/bdev/Makefile? Because the Makefile builds the modules; this list tells the SPDK app targets which ones to link statically. If you only add to the Makefile and not to this list, your module's .so exists but the app targets won't link it in.

ListPurposeWhen to add your module
BLOCKDEV_MODULES_LISTModules linked into the SPDK app targetsAlways (if your module should be in the default build)
INTR_BLOCKDEV_MODULES_LISTModules that can run in interrupt mode (no poller)Only if your module doesn't need a reactor poller
BLOCKDEV_MODULES_PRIVATE_LIBSExternal libraries to link your module againstOnly if your module needs an external lib (e.g. -liscsi)

The convention: simple in-process modules (bdev_malloc, bdev_null, bdev_passthru) go in BLOCKDEV_MODULES_LIST alphabetically within the first line; modules with optional deps (bdev_crypto, bdev_ocf, bdev_aio) get their own ifeq ($(CONFIG_XXX),y) block lower in the file.

4. The configure script

The configure script is a 1,000+ line bash script that detects available libraries and sets CONFIG_∗ variables in a generated Makefile. For most bdev modules you don't need to touch it: passthru, malloc, null, error, delay, split, gpt, raid, zone_block are all unconditionally built.

For modules that need an optional external dependency (e.g. RBD needs librbd, iSCSI needs libiscsi, OCF needs the OCF cache library), you add a --with-XXX flag. The pattern in configure is:

And the corresponding detection logic lives further down in the file (around the line that says --with-rbd) in the case statement). For a new optional module, you'd add:

  1. A help-text line in the echo block at the top.

  2. A --with-mymod) case in the case statement below.

  3. A pkg-config probe or a header check to set CONFIG_MYMOD.

The result is that the user runs ./configure --with-mymod and the build system picks up the module. Without the flag, the module isn't built and the DIRS-$(CONFIG_MYMOD) line in module/bdev/Makefile is empty.

5. JSON-RPC registration

If your module needs to be created at runtime (and almost every bdev does), you need a JSON-RPC method. The pattern: a companion <name>_rpc.c file that defines a struct for the parameters, a decoder table, a handler function, and a SPDK_RPC_REGISTER call.

Two macros:

  1. SPDK_RPC_REGISTER(method, func, state_mask) — the standard one. method is the JSON-RPC method name (e.g. "bdev_passthru_create"), func is the C function that handles it, and state_mask is SPDK_RPC_STARTUP or SPDK_RPC_RUNTIME. SPDK_RPC_RUNTIME is what you want: the method is available at any time after the framework starts.

  2. SPDK_RPC_REGISTER_ALIAS_DEPRECATED(method, alias) — for renaming an RPC. The method keeps working but emits a deprecation warning; the alias is the new name. The priority (1001) is higher than the standard register (1000) so aliases are registered after methods, ensuring the original method exists before the alias is created.

The full RPC pattern

For each RPC, you need five pieces in your <name>_rpc.c file:

  1. Parameter struct — one field per JSON parameter, all char∗ for strings, struct spdk_uuid for UUIDs, and so on.

  2. Decoder table — one row per parameter, with the JSON field name, the offset within the struct, and the decoder function. The last column is true for optional fields.

  3. Free function — frees all strdup'd strings after the RPC is done. The framework doesn't know which fields need freeing.

  4. Handler — the five-step pattern: decode, call the create function, build the response, send it (or send an error), clean up.

  5. One SPDK_RPC_REGISTER line per method.

The full bdev_passthru_create handler is on the previous page at module/bdev/passthru/vbdev_passthru_rpc.c:38 . Use it as a template.

Registering multiple RPCs

The convention is one SPDK_RPC_REGISTER line per method, all at the bottom of the <name>_rpc.c file. The order doesn't matter functionally — the constructor priority (1000) is the same for all of them, so all are registered before main() runs. The pattern:

SPDK_RPC_REGISTER("my_bdev_create",  rpc_my_bdev_create,  SPDK_RPC_RUNTIME)
SPDK_RPC_REGISTER("my_bdev_delete",  rpc_my_bdev_delete,  SPDK_RPC_RUNTIME)
SPDK_RPC_REGISTER("my_bdev_resize",  rpc_my_bdev_resize,  SPDK_RPC_RUNTIME)

All three are now callable via JSON-RPC. The user invokes them with:

rpc.py my_bdev_create  '{"base_bdev_name": "Malloc0", "name": "Passthru0"}'

6. Module documentation (optional)

SPDK's docs live in doc/. The relevant files for a bdev module are:

FileWhat to add
doc/bdev.mdA row in the bdev module table with your module name and a one-line description
doc/<name>.md(Optional) A longer doc page with config-file examples, RPC examples, and known limitations
doc/jsonrpc/<name>/(Optional) Per-RPC docs autogenerated by Doxygen

For a quick-win first module, just update doc/bdev.md with a one-line entry. The longer doc page is worth writing if your module is going to be used by anyone other than yourself.

7. Testing with bdevperf

The single best way to test a new bdev module is with examples/bdev/bdevperf. It's a small SPDK app that:

  1. Reads a config file with one or more bdevs.
  2. Submits reads, writes, and unmap operations against each bdev.
  3. Reports throughput, IOPS, and latency.

The pattern:

  # build SPDK with your module
  cd /path/to/spdk
  ./configure
  make -j

  # write a config file
  cat > my_bdev.conf <<EOF
  [Malloc]
  NumberOfLuns 1
  LunSizeInMB 128

  [MyBdev]
  Malloc0 Passthru0
  EOF

  # run bdevperf with the config
  ./build/examples/bdevperf -c my_bdev.conf

  # in another terminal, exercise the bdev
  ./scripts/rpc.py bdev_passthru_create '{"base_bdev_name": "Malloc0", "name": "MyPassthru0"}'
  ./scripts/rpc.py bdev_get_bdevs
  ./scripts/rpc.py bdevperf_test_qd     '{"qdepth": 16, "io_count": 1000, "bdev_name": "MyPassthru0"}'
  ./scripts/rpc.py save_config

For each I/O type your module supports, run a test that exercises it:

I/O typeHow to test
READ / WRITEbdevperf_test_qd with default options
UNMAPbdevperf_test_qd with unmap in io_type
WRITE_ZEROESbdevperf_test_qd with write_zeroes in io_type
FLUSHbdevperf_test_qd with flush in io_type
Hot-removeManually: unregister the base bdev via RPC; verify your passthru is also torn down
Persistencesave_config → check the JSON file includes your bdev → restart the process → verify the bdev reappears

Edge cases: build-time footguns

Module name conflict

If your module's .name in the spdk_bdev_module struct is the same as another loaded module's, spdk_bdev_module_list_add appends yours to the list but the framework's spdk_bdev_module_list_find always returns the first match. The result: RPCs that look up the module by name hit the wrong one. The fix: pick a unique name. The convention is bdev_<thing> for the library and <thing> for the module name.

Forgetting to add to BLOCKDEV_MODULES_LIST

Your .so builds, but the SPDK app targets don't link it in. The SPDK_BDEV_MODULE_REGISTER constructor runs (because the .so is loaded for the unit tests), but at runtime the framework's module list doesn't include your module because the app binary doesn't link your .a. The fix: always add to BLOCKDEV_MODULES_LIST.

Syntax error in the Makefile

make errors out with a line number. Easy to fix. The most common mistake is a tab-vs-space issue: Makefiles require tabs, not spaces, for command lines. The DIRS-y += my_mod line is fine either way; the SPDK_ROOT_DIR := ... line is also fine. But if you ever add a multi-line recipe, watch the tabs.

The constructor doesn't run

If your SPDK_BDEV_MODULE_REGISTER(passthru, &passthru_if) is missing or commented out, your module's .name never gets added to the framework's list, and the module is invisible. There's no error — the framework just doesn't know your module exists. The fix: always have the register line. Always.

Wrong shared library version

If you increment SO_VER (the major version) but downstream users have the old libbdev_<name>.so.6 symlink, they get an unresolved-symbol error. If you bump SO_MINOR without SO_VER, ABI breaks go unnoticed. The discipline: only bump SO_MINOR for backward-compatible changes; bump SO_VER when you change any struct, function signature, or exported symbol.

RPC method not found at runtime

You registered the RPC in your <name>_rpc.c file, but the user gets method not found. Cause: the <name>_rpc.c file is in C_SRCS in your Makefile, but the SPDK_RPC_REGISTER macro didn't run because the file wasn't compiled. Check C_SRCS; check the .o file exists in the build output; check the constructor priority (must be 1000 or higher).

Scaffolding skeleton: a complete .c template

Copy this into a file and start filling in the // TODO comments. The skeleton follows the conventions of vbdev_passthru.c:

  /*   SPDX-License-Identifier: BSD-3-Clause
   *   Copyright (C) 2024 Your Name.
   *   All rights reserved.
   */

  /*
   * Module description goes here.
   */

  #include "spdk/stdinc.h"

  #include "spdk/bdev.h"
  #include "spdk/bdev_module.h"
  #include "spdk/thread.h"
  #include "spdk/jsonrpc.h"
  #include "spdk/rpc.h"
  #include "spdk/string.h"
  #include "spdk/log.h"
  #include "spdk/env.h"

  #include "vbdev_mybdev.h"

  #define BDEV_MYBDEV_NAMESPACE_UUID "00000000-0000-0000-0000-000000000000"

  /* === per-bdev struct (one per bdev) === */
  struct vbdev_mybdev {
      struct spdk_bdev        bdev;       /* the framework-visible bdev */
      struct spdk_bdev_desc   *base_desc; /* desc on the base bdev, if any */
      struct spdk_bdev        *base_bdev; /* the base bdev, if a vbdev */
      struct spdk_thread      *thread;    /* thread where base_desc was opened */
      TAILQ_ENTRY(vbdev_mybdev) link;
  };

  /* === per-thread channel === */
  struct mybdev_io_channel {
      struct spdk_io_channel  *base_ch;   /* cached base channel, if a vbdev */
      /* TODO: per-thread state for leaf modules goes here */
  };

  /* === per-IO context (sized via get_ctx_size) === */
  struct mybdev_io {
      uint8_t                 marker;     /* round-trip sanity check */
      struct spdk_io_channel  *ch;        /* for NOMEM-queue resubmit */
      struct spdk_bdev_io_wait_entry wait; /* for bdev_io_wait queue */
  };

  static TAILQ_HEAD(, vbdev_mybdev) g_mybdev_nodes =
      TAILQ_HEAD_INITIALIZER(g_mybdev_nodes);

  /* === forward declarations === */
  static int  mybdev_init(void);
  static int  mybdev_get_ctx_size(void);
  static void mybdev_finish(void);
  static int  mybdev_config_json(struct spdk_json_write_ctx *w);
  static int  mybdev_destruct(void *ctx);
  static void mybdev_submit_request(struct spdk_io_channel *ch,
                                   struct spdk_bdev_io *bdev_io);
  static bool mybdev_io_type_supported(void *ctx, enum spdk_bdev_io_type);
  static struct spdk_io_channel *mybdev_get_io_channel(void *ctx);
  static int  mybdev_ch_create_cb(void *io_device, void *ctx_buf);
  static void mybdev_ch_destroy_cb(void *io_device, void *ctx_buf);
  static void mybdev_dump_info_json(void *ctx, struct spdk_json_write_ctx *w);
  static void mybdev_write_config_json(struct spdk_bdev *bdev,
                                       struct spdk_json_write_ctx *w);

  /* === module struct === */
  static struct spdk_bdev_module mybdev_if = {
      .name           = "mybdev",
      .module_init    = mybdev_init,
      .module_fini    = mybdev_finish,
      .get_ctx_size   = mybdev_get_ctx_size,
      .config_json    = mybdev_config_json,
      /* for vbdev: .examine_config = mybdev_examine, */
  };

  SPDK_BDEV_MODULE_REGISTER(mybdev, &mybdev_if)

  /* === fn_table === */
  static const struct spdk_bdev_fn_table mybdev_fn_table = {
      .destruct            = mybdev_destruct,
      .submit_request      = mybdev_submit_request,
      .io_type_supported   = mybdev_io_type_supported,
      .get_io_channel      = mybdev_get_io_channel,
      .dump_info_json      = mybdev_dump_info_json,
      .write_config_json   = mybdev_write_config_json,
  };

  /* === module init === */
  static int
  mybdev_init(void)
  {
      /* TODO: per-module setup. For a leaf module, register an io_device
       * here. For a vbdev, nothing to do — examine_config is the entry. */
      return 0;
  }

  /* === module fini === */
  static void
  mybdev_finish(void)
  {
      /* TODO: free any per-module state allocated in init. */
  }

  /* === per-IO scratch size === */
  static int
  mybdev_get_ctx_size(void)
  {
      return sizeof(struct mybdev_io);
  }

  /* === teardown === */
  static int
  mybdev_destruct(void *ctx)
  {
      struct vbdev_mybdev *mbdev = ctx;

      /* 1. remove from global list first */
      TAILQ_REMOVE(&g_mybdev_nodes, mbdev, link);

      /* 2. release claim (vbdev only) */
      if (mbdev->base_bdev) {
          spdk_bdev_module_release_bdev(mbdev->base_bdev);
      }

      /* 3. close base desc on its opening thread (vbdev only) */
      if (mbdev->base_desc) {
          if (mbdev->thread && mbdev->thread != spdk_get_thread()) {
              spdk_thread_send_msg(mbdev->thread, _mybdev_close_desc,
                                   mbdev->base_desc);
          } else {
              spdk_bdev_close(mbdev->base_desc);
          }
      }

      /* 4. unregister io_device */
      spdk_io_device_unregister(mbdev, _mybdev_unregister_cb);

      return 0;
  }

  /* === hot path === */
  static void
  mybdev_submit_request(struct spdk_io_channel *ch, struct spdk_bdev_io *bdev_io)
  {
      struct mybdev_io *io_ctx = (struct mybdev_io *)bdev_io->driver_ctx;
      struct vbdev_mybdev *mbdev;
      struct mybdev_io_channel *mch;
      int rc = 0;

      /* TODO: For a leaf module, do the I/O directly. For a vbdev,
       * forward to the base bdev. */
      mch = spdk_io_channel_get_ctx(ch);
      io_ctx->marker = 0xa5;

      switch (bdev_io->type) {
      case SPDK_BDEV_IO_TYPE_READ:
          /* TODO: submit a read */
          spdk_bdev_io_complete(bdev_io, SPDK_BDEV_IO_STATUS_FAILED);
          return;
      case SPDK_BDEV_IO_TYPE_WRITE:
          /* TODO: submit a write */
          spdk_bdev_io_complete(bdev_io, SPDK_BDEV_IO_STATUS_FAILED);
          return;
      case SPDK_BDEV_IO_TYPE_FLUSH:
      case SPDK_BDEV_IO_TYPE_RESET:
          spdk_bdev_io_complete(bdev_io, SPDK_BDEV_IO_STATUS_SUCCESS);
          return;
      default:
          spdk_bdev_io_complete(bdev_io, SPDK_BDEV_IO_STATUS_FAILED);
          return;
      }
  }

  /* === io_type_supported === */
  static bool
  mybdev_io_type_supported(void *ctx, enum spdk_bdev_io_type type)
  {
      /* TODO: return true for types you actually handle in submit_request */
      switch (type) {
      case SPDK_BDEV_IO_TYPE_READ:
      case SPDK_BDEV_IO_TYPE_WRITE:
      case SPDK_BDEV_IO_TYPE_FLUSH:
      case SPDK_BDEV_IO_TYPE_RESET:
          return true;
      default:
          return false;
      }
  }

  /* === get_io_channel === */
  static struct spdk_io_channel *
  mybdev_get_io_channel(void *ctx)
  {
      return spdk_get_io_channel(ctx);
  }

  /* === channel create === */
  static int
  mybdev_ch_create_cb(void *io_device, void *ctx_buf)
  {
      struct mybdev_io_channel *mch = ctx_buf;
      struct vbdev_mybdev *mbdev = io_device;

      /* TODO: initialize the channel. If a vbdev, grab the base channel. */
      if (mbdev->base_desc) {
          mch->base_ch = spdk_bdev_get_io_channel(mbdev->base_desc);
      }
      return 0;
  }

  /* === channel destroy === */
  static void
  mybdev_ch_destroy_cb(void *io_device, void *ctx_buf)
  {
      struct mybdev_io_channel *mch = ctx_buf;

      if (mch->base_ch) {
          spdk_put_io_channel(mch->base_ch);
      }
  }

  /* === dump_info_json === */
  static void
  mybdev_dump_info_json(void *ctx, struct spdk_json_write_ctx *w)
  {
      struct vbdev_mybdev *mbdev = ctx;

      spdk_json_write_name(w, "mybdev");
      spdk_json_write_object_begin(w);
      spdk_json_write_named_string(w, "name", spdk_bdev_get_name(&mbdev->bdev));
      spdk_json_write_object_end(w);
  }

  /* === write_config_json === */
  static void
  mybdev_write_config_json(struct spdk_bdev *bdev, struct spdk_json_write_ctx *w)
  {
      /* TODO: emit the RPC to recreate this bdev */
  }

  /* === module-level config_json (alternative to fn_table->write_config_json) === */
  static int
  mybdev_config_json(struct spdk_json_write_ctx *w)
  {
      struct vbdev_mybdev *mbdev;

      TAILQ_FOREACH(mbdev, &g_mybdev_nodes, link) {
          spdk_json_write_object_begin(w);
          spdk_json_write_named_string(w, "method", "mybdev_create");
          spdk_json_write_named_object_begin(w, "params");
          /* TODO: emit the parameters your create RPC accepts */
          spdk_json_write_object_end(w);
          spdk_json_write_object_end(w);
      }
      return 0;
  }

  /* === public API for RPC handler === */
  int
  vbdev_mybdev_create(const char *base_bdev_name, const char *name)
  {
      struct vbdev_mybdev *mbdev;
      struct spdk_bdev *base;
      int rc;

      /* TODO: implement construction. Pattern is the same as
       * vbdev_passthru_register: open the base, copy geometry,
       * register io_device, claim, register bdev. */
      mbdev = calloc(1, sizeof(*mbdev));
      if (!mbdev) {
          return -ENOMEM;
      }

      mbdev->bdev.name = strdup(name);
      if (!mbdev->bdev.name) {
          free(mbdev);
          return -ENOMEM;
      }
      mbdev->bdev.product_name = "mybdev";
      mbdev->bdev.ctxt = mbdev;
      mbdev->bdev.fn_table = &mybdev_fn_table;
      mbdev->bdev.module = &mybdev_if;

      rc = spdk_bdev_open_ext(base_bdev_name, true, NULL, NULL,
                              &mbdev->base_desc);
      if (rc) {
          free(mbdev->bdev.name);
          free(mbdev);
          return rc;
      }
      base = spdk_bdev_desc_get_bdev(mbdev->base_desc);
      mbdev->base_bdev = base;

      /* copy geometry from base */
      mbdev->bdev.blocklen = base->blocklen;
      mbdev->bdev.blockcnt = base->blockcnt;
      mbdev->bdev.required_alignment = base->required_alignment;

      mbdev->thread = spdk_get_thread();

      spdk_io_device_register(mbdev, mybdev_ch_create_cb,
                              mybdev_ch_destroy_cb,
                              sizeof(struct mybdev_io_channel),
                              name);

      rc = spdk_bdev_module_claim_bdev(base, mbdev->base_desc,
                                       &mybdev_if);
      if (rc) {
          spdk_bdev_close(mbdev->base_desc);
          spdk_io_device_unregister(mbdev, NULL);
          free(mbdev->bdev.name);
          free(mbdev);
          return rc;
      }

      TAILQ_INSERT_TAIL(&g_mybdev_nodes, mbdev, link);

      rc = spdk_bdev_register(&mbdev->bdev);
      if (rc) {
          spdk_bdev_module_release_bdev(base);
          spdk_bdev_close(mbdev->base_desc);
          TAILQ_REMOVE(&g_mybdev_nodes, mbdev, link);
          spdk_io_device_unregister(mbdev, NULL);
          free(mbdev->bdev.name);
          free(mbdev);
          return rc;
      }

      return 0;
  }

  /* === helpers for destruct === */
  static void
  _mybdev_close_desc(void *ctx)
  {
      spdk_bdev_close(ctx);
  }

  static void
  _mybdev_unregister_cb(void *io_device)
  {
      struct vbdev_mybdev *mbdev = io_device;
      free(mbdev->bdev.name);
      free(mbdev);
  }

  SPDK_LOG_REGISTER_COMPONENT(vbdev_mybdev)

The skeleton has every section in place. To use it:

  1. Save as module/bdev/mybdev/vbdev_mybdev.c.
  2. Copy module/bdev/passthru/Makefile to module/bdev/mybdev/Makefile and update the LIBNAME and C_SRCS lines.

  3. Add mybdev to DIRS-y in module/bdev/Makefile.

  4. Add bdev_mybdev to BLOCKDEV_MODULES_LIST in mk/spdk.modules.mk.

  5. Add an SPDK_RPC_REGISTER("mybdev_create", ...) call in your vbdev_mybdev_rpc.c file (modeled on module/bdev/passthru/vbdev_passthru_rpc.c:38 ).

  6. Implement the // TODO sections.

  7. make -j and test with examples/bdev/bdevperf.

What to take away

The build glue is mostly silent: missing entries don't error, they just make your module invisible. Build verification into your workflow: after every change, bdev_get_bdevs should list your module, and rpc.py <your_method> should be callable. If either is missing, the bug is in the build glue, not in the C source.

The seven touchpoints: per-module Makefile, parent Makefile, modules list, configure (optional), JSON-RPC registration, doc, and bdevperf testing. You now know what each one does and what happens when you forget it. The skeleton at the bottom of this page is copy-paste ready: replace the // TODO comments and you have a working module.