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.
- The seven build touchpoints
- 1. The per-module
Makefile - 2. The parent
module/bdev/Makefile - 3.
mk/spdk.modules.mk— the modules list - 4. The
configurescript - 5. JSON-RPC registration
- 6. Module documentation (optional)
- 7. Testing with
bdevperf - Edge cases: build-time footguns
- Scaffolding skeleton: a complete
.ctemplate
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.
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 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:
SPDK_ROOT_DIR := $(abspath $(CURDIR)/../../..)— walk up three directories from the module's Makefile to the SPDK repo root. Theabspathis important: it lets the build work from any cwd.include $(SPDK_ROOT_DIR)/mk/spdk.common.mk— common build flags, dependency tracking, output directory setup. Required.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.cfiles, list them all.LIBNAME = bdev_passthru— the output library name. The convention isbdev_<name>. This produceslibbdev_passthru.so(and.so.7.0as a versioned symlink).include $(SPDK_ROOT_DIR)/mk/spdk.lib.mk— the rules for building a library. Required. This is what turnsC_SRCSandLIBNAMEinto the actual.oand.sofiles.
Two optional pieces:
SO_VER := 7andSO_MINOR := 0— the shared library version. IncrementSO_VERwhen you break ABI; bumpSO_MINORfor backward-compatible changes. The malloc and null modules use the same values; the convention is to keep all bdev modules on the same version.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 getsdlopen()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:
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.If the module requires an optional dependency, add it to a
DIRS-$(CONFIG_XXX)line. TheCONFIG_XXXvariable is set by theconfigurescript based on which--with-XXXflags 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.
| List | Purpose | When to add your module |
|---|---|---|
BLOCKDEV_MODULES_LIST | Modules linked into the SPDK app targets | Always (if your module should be in the default build) |
INTR_BLOCKDEV_MODULES_LIST | Modules that can run in interrupt mode (no poller) | Only if your module doesn't need a reactor poller |
BLOCKDEV_MODULES_PRIVATE_LIBS | External libraries to link your module against | Only 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:
A help-text line in the
echoblock at the top.A
--with-mymod)case in thecasestatement below.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:
SPDK_RPC_REGISTER(method, func, state_mask)— the standard one.methodis the JSON-RPC method name (e.g."bdev_passthru_create"),funcis the C function that handles it, andstate_maskisSPDK_RPC_STARTUPorSPDK_RPC_RUNTIME.SPDK_RPC_RUNTIMEis what you want: the method is available at any time after the framework starts.SPDK_RPC_REGISTER_ALIAS_DEPRECATED(method, alias)— for renaming an RPC. Themethodkeeps working but emits a deprecation warning; thealiasis 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:
Parameter struct — one field per JSON parameter, all
char∗for strings,struct spdk_uuidfor UUIDs, and so on.Decoder table — one row per parameter, with the JSON field name, the offset within the struct, and the decoder function. The last column is
truefor optional fields.Free function — frees all
strdup'd strings after the RPC is done. The framework doesn't know which fields need freeing.Handler — the five-step pattern: decode, call the create function, build the response, send it (or send an error), clean up.
One
SPDK_RPC_REGISTERline 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:
| File | What to add |
|---|---|
doc/bdev.md | A 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:
- Reads a config file with one or more bdevs.
- Submits reads, writes, and unmap operations against each bdev.
- 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_configFor each I/O type your module supports, run a test that exercises it:
| I/O type | How to test |
|---|---|
| READ / WRITE | bdevperf_test_qd with default options |
| UNMAP | bdevperf_test_qd with unmap in io_type |
| WRITE_ZEROES | bdevperf_test_qd with write_zeroes in io_type |
| FLUSH | bdevperf_test_qd with flush in io_type |
| Hot-remove | Manually: unregister the base bdev via RPC; verify your passthru is also torn down |
| Persistence | save_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:
- Save as
module/bdev/mybdev/vbdev_mybdev.c. Copy
module/bdev/passthru/Makefiletomodule/bdev/mybdev/Makefileand update theLIBNAMEandC_SRCSlines.Add
mybdevtoDIRS-yinmodule/bdev/Makefile.Add
bdev_mybdevtoBLOCKDEV_MODULES_LISTinmk/spdk.modules.mk.Add an
SPDK_RPC_REGISTER("mybdev_create", ...)call in yourvbdev_mybdev_rpc.cfile (modeled on module/bdev/passthru/vbdev_passthru_rpc.c:38 ).Implement the
// TODOsections.make -jand test withexamples/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.