API Code Examples#

This section includes three examples of using the xNVMe library. These are gradually increasing in complexity, and are meant to complement the description of the Core API.

All of these examples can be run even without having an NVMe device. If you give the argument 1GB on the command-line, xNVMe will recognize that you want to use the ramdisk backend.

In the source repository, you will find more usage examples in the examples folder.

Hello World#

In this example we open a device with the uri provided on the command line. This is done by using the default xnvme_opts, but this struct is where you would specify the desired backend, sync/async/admin command interface, etc.

// SPDX-FileCopyrightText: Samsung Electronics Co., Ltd
//
// SPDX-License-Identifier: BSD-3-Clause

#include <errno.h>
#include <libxnvme.h>

int
main(int argc, char **argv)
{
	struct xnvme_opts opts = xnvme_opts_default();
	struct xnvme_dev *dev;

	if (argc < 2) {
		xnvme_cli_perr("Usage: %s <identifer>", EINVAL);
		return 1;
	}

	dev = xnvme_dev_open(argv[1], &opts);
	if (!dev) {
		xnvme_cli_perr("xnvme_dev_open()", errno);
		return 1;
	}

	if (xnvme_dev_derive_geo(dev)) {
		printf("Failed deriving geometry, check your permissions.");
	}
	xnvme_dev_pr(dev, XNVME_PR_DEF);
	xnvme_dev_close(dev);

	return 0;
}

Synchronous I/O#

In this example we open a device and send a single synchronous read command. Note, when doing synchronous IO, there is no need for a command-queue. Instead, you get the command-context directly from the device with xnvme_cmd_ctx_from_dev(). The example is intended to be easily extended by adding a for-loop and submitting multiple commands.

// SPDX-FileCopyrightText: Samsung Electronics Co., Ltd
//
// SPDX-License-Identifier: BSD-3-Clause

#include <errno.h>
#include <libxnvme.h>

/**
 * This example shows how send a single synchronous command
 *
 * - Allocate a buffer for the command payload
 * - Submit the asynchronous command
 * - Reporting on IO-errors
 * - Teardown
 */
int
main(int argc, char **argv)
{
	struct xnvme_opts opts = xnvme_opts_default();
	struct xnvme_dev *dev = NULL;
	uint32_t nsid;
	char *buf = NULL;
	size_t buf_nbytes;
	int err = 0;

	if (argc < 2) {
		xnvme_cli_perr("Usage: %s <ident>", EINVAL);
		return 1;
	}

	dev = xnvme_dev_open(argv[1], &opts);
	if (!dev) {
		xnvme_cli_perr("xnvme_dev_open()", errno);
		return 1;
	}
	nsid = xnvme_dev_get_nsid(dev);

	buf_nbytes = xnvme_dev_get_geo(dev)->nbytes;

	xnvme_cli_pinf("Allocate a payload-buffer of nbytes: %zu", buf_nbytes);
	buf = xnvme_buf_alloc(dev, buf_nbytes);
	if (!buf) {
		xnvme_cli_perr("xnvme_buf_alloc()", errno);
		goto exit;
	}
	memset(buf, 0, buf_nbytes);

	// This block would typically be a loop as more than a single command would be submitted
	{
		struct xnvme_cmd_ctx ctx = xnvme_cmd_ctx_from_dev(dev);

		// Submit and wait for the completion of a read command
		err = xnvme_nvm_read(&ctx, nsid, 0x0, 0, buf, NULL);
		if (err || xnvme_cmd_ctx_cpl_status(&ctx)) {
			xnvme_cli_perr("xnvme_nvm_read()", err);
			xnvme_cmd_ctx_pr(&ctx, XNVME_PR_DEF);
			err = err ? err : -EIO;
			goto exit;
		}
		xnvme_cli_pinf("Submitted and completed command succesfully");
	}

exit:
	xnvme_buf_free(dev, buf);
	xnvme_dev_close(dev);

	return err;
}

Asynchronous I/O#

The typical flow for doing asynchronous IO can be reduced to the following:

  • Open the device with xnvme_dev_open()

  • Initialize the command queue with xnvme_queue_init()

  • Set the queue callback with xnvme_queue_set_cb()

  • Get a command context from the queue with xnvme_queue_get_cmd_ctx()

  • Submit an I/O command, such as with xnvme_nvm_read() (see libxnvme_nvm.h for more options)

  • Get completion with xnvme_queue_poke()

  • Terminate the command queue with xnvme_queue_term()

In this example, we open a device and send a single asynchronous read command. The process follows these steps:

  1. Initialize the Command Queue: The xnvme_queue, referred to as the command queue, is initialized with a number of command contexts equal to its capacity.

  2. Submit a Command: To submit a command, you obtain a command context from the command queue.

  3. Process Completed Commands: When processing completed commands, you poke the command queue, and a callback function is called for each command context. At the end of the callback function, you should return the command context to the command queue.

The example is intended to be easily extended by adding a for-loop and submitting multiple commands.

// SPDX-FileCopyrightText: Samsung Electronics Co., Ltd
//
// SPDX-License-Identifier: BSD-3-Clause

#include <errno.h>
#include <libxnvme.h>

static void
cb_fn(struct xnvme_cmd_ctx *ctx, void *XNVME_UNUSED(cb_arg))
{
	if (xnvme_cmd_ctx_cpl_status(ctx)) {
		xnvme_cli_pinf("Command did not complete successfully");
		xnvme_cmd_ctx_pr(ctx, XNVME_PR_DEF);
	} else {
		xnvme_cli_pinf("Command completed succesfully");
	}

	// Completed: Put the command-context back in the queue
	xnvme_queue_put_cmd_ctx(ctx->async.queue, ctx);
}

/**
 * This example shows how send a single asynchronous command
 *
 * - Allocate a buffer for the command payload
 * - Setup a Command Queue
 *   | Callback functions and callback arguments
 *   | Using command-contexts
 * - Submit the asynchronous command
 *   | Re-submission when busy, reap completion, waiting till empty
 * - Teardown
 */
int
main(int argc, char **argv)
{
	struct xnvme_opts opts = xnvme_opts_default();
	struct xnvme_dev *dev = NULL;
	uint32_t nsid;
	char *buf = NULL;
	size_t buf_nbytes;
	struct xnvme_queue *queue = NULL;
	const int qdepth = 16;
	int ret = 0, err = 0;

	if (argc < 2) {
		xnvme_cli_perr("Usage: %s <ident>", EINVAL);
		return 1;
	}

	dev = xnvme_dev_open(argv[1], &opts);
	if (!dev) {
		xnvme_cli_perr("xnvme_dev_open()", errno);
		return 1;
	}
	nsid = xnvme_dev_get_nsid(dev);

	// Initialize a command-queue
	ret = xnvme_queue_init(dev, qdepth, 0, &queue);
	if (ret) {
		xnvme_cli_perr("xnvme_queue_init()", ret);
		xnvme_dev_close(dev);
		return 1;
	}

	// Set the callback function
	// Note: for this example the cb_arg is NULL
	// However, it would typically be a pointer to a struct
	xnvme_queue_set_cb(queue, cb_fn, NULL);

	buf_nbytes = xnvme_dev_get_geo(dev)->nbytes;

	xnvme_cli_pinf("Allocate a payload-buffer of nbytes: %zu", buf_nbytes);
	buf = xnvme_buf_alloc(dev, buf_nbytes);
	if (!buf) {
		xnvme_cli_perr("xnvme_buf_alloc()", errno);
		goto exit;
	}
	memset(buf, 0, buf_nbytes);

	// This block would typically be a loop as more than a single command would be submitted
	{
		struct xnvme_cmd_ctx *ctx = xnvme_queue_get_cmd_ctx(queue);

submit:
		// Submit a read command
		err = xnvme_nvm_read(ctx, nsid, 0x0, 0, buf, NULL);
		switch (err) {
		case 0:
			xnvme_cli_pinf("Submission succeeded");
			break;

		// Submission failed: queue is full => process completions and try again
		case -EBUSY:
		case -EAGAIN:
			xnvme_queue_poke(queue, 0);
			goto submit;

		// Submission failed: unexpected error, put the command-context back in the queue
		default:
			xnvme_cli_perr("xnvme_nvm_read()", err);
			xnvme_queue_put_cmd_ctx(queue, ctx);
			goto exit;
		}
	}

	// Done submitting, wait for all outstanding I/O to complete as we are about to exit
	ret = xnvme_queue_drain(queue);
	if (ret < 0) {
		xnvme_cli_perr("xnvme_queue_drain()", ret);
		goto exit;
	}

exit:
	xnvme_buf_free(dev, buf);
	xnvme_queue_term(queue);
	xnvme_dev_close(dev);

	return ret || err;
}