From: Jiri Pirko <jiri@resnulli.us>
To: linux-coco@lists.linux.dev
Cc: linux-kernel@vger.kernel.org, linux-api@vger.kernel.org,
x86@kernel.org, kvm@vger.kernel.org, linux-doc@vger.kernel.org,
jgg@ziepe.ca, david.quigley@intel.com, lukas@wunner.de,
ksinyuk@kernel.org, rodrigo.vivi@intel.com,
cedric.xing@intel.com, sameo@meta.com, drdeeglaze@gmail.com,
James.Bottomley@HansenPartnership.com, aik@amd.com,
jpiotrowski@nvidia.com, qinkun@google.com, pgonda@google.com,
jic23@kernel.org, alistair.francis@wdc.com, ankita@nvidia.com,
suzuki.poulose@arm.com, kas@kernel.org,
rick.p.edgecombe@intel.com, elena.reshetova@intel.com,
dave.hansen@linux.intel.com,
sathyanarayanan.kuppuswamy@linux.intel.com, peter.fang@intel.com,
jarkko@kernel.org, zohar@linux.ibm.com, yeoreum.yun@arm.com,
aneesh.kumar@kernel.org, dave.jiang@intel.com,
nik.borisov@suse.com, sergii.dmytruk@3mdeb.com
Subject: [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family
Date: Fri, 13 Nov 2026 16:55:12 +0100 [thread overview]
Message-ID: <20261113155515.2510194-14-jiri@resnulli.us> (raw)
In-Reply-To: <20261113155515.2510194-1-jiri@resnulli.us>
From: Jiri Pirko <jiri@nvidia.com>
Describe the YAML language, generated family descriptors, lifecycle,
operations, blobs, inheritance and compatible extension rules.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- fixed the family descriptor name in the ctlv_create() example
- documented ctlv_record_cancel()
- stated how the generator keeps every message within the limit
- dropped the events
- reworked the guide after review, around a worked example family
- warned that a family's registry position is its ID
- renamed stubs to extension points
- added the kernel-doc of the family API
- described specs as op trees and records
- said to return ctlv_blob_too_short() for an output blob too short
- said what validation accepted instead of placements
- said record instead of nest, the root being one
- took the UAPI headers from include/uapi/ctlv/
- documented every item of the example family's spec
- generated and recorded a family's ABI with one regen, and listed
the files it should change
- described enum extension points
- said how to name an op that reads
- described a subsystem's nodes in place of the ctlv class
- said message instead of wire
---
Documentation/driver-api/ctlv.rst | 716 +++++++++++++++++++++++++++++
Documentation/driver-api/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 718 insertions(+)
create mode 100644 Documentation/driver-api/ctlv.rst
diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
new file mode 100644
index 000000000000..34d20c14a9bf
--- /dev/null
+++ b/Documentation/driver-api/ctlv.rst
@@ -0,0 +1,716 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+=====================
+Writing a CTLV family
+=====================
+
+:Author: Jiri Pirko
+
+A CTLV family is a set of system calls for one sort of device: actions and
+queries a program executes. The family is written as a
+YAML specification, a generator turns that into headers, and the family
+implementation supplies the callbacks the generated header names. CTLV checks
+every request against the specification before a callback sees it. The
+userspace ABI is :doc:`/userspace-api/ctlv`; the in-tree specifications are
+under ``Documentation/ctlv/specs``.
+
+The example
+===========
+
+This guide writes a small invented family, ``ctlv-example``, with two system
+calls::
+
+ item_set_action(u64 index, string label?, blob data?) -> status;
+ item_get_query() -> { u64 index; string label?; }[];
+
+``index`` is 0..4095, ``label`` is at most 63 bytes excluding NUL, and
+``data`` is at most 1 MiB with 8-byte alignment. ``?`` marks an optional
+argument. An action runs once and returns a status. A query returns an answer
+that may be larger than the caller's buffer, which CTLV delivers in as many
+fragments as the buffer needs; a list, as here, is the usual case.
+
+Write the specification
+=======================
+
+The specification starts with the family. ``ctlv-example`` inherits ``ctlv``,
+the root family every device answers:
+
+.. code-block:: yaml
+
+ # SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+ %YAML 1.2
+ ---
+ # Numbered items, each with an optional label.
+ family: ctlv-example
+ inherits: ctlv
+
+The comment on the lines above an entry is its documentation, which the
+generated headers carry. The generator refuses a family, record, field, op,
+enum or flags without one, and an enum value or a flag without its own, so
+values go one to a line.
+
+Each system call is an op:
+
+.. code-block:: yaml
+
+ ops:
+ # Sets an item's label and data.
+ item-set:
+ type: action
+ request:
+ # Which item it is.
+ index: uint(..4095)
+ # What the item is called.
+ label?: string(..63)
+ # The item's contents, which the device reads.
+ data?: blob(in, ..1M, align 8, va)
+
+ # Lists the items.
+ item-get:
+ type: query
+ reply:
+ # Every item the device keeps.
+ items[]:
+ # Which item it is.
+ index: uint(..4095)
+ # What the item is called.
+ label?: string(..63)
+
+``request`` is what an op takes and ``reply`` what it returns. An op
+without a ``request`` takes no arguments, and an action without a ``reply``
+returns only its status. An action or a query may also set these keys:
+
+=========================== ========== ========================================
+Key Default What it does
+=========================== ========== ========================================
+``fd-access`` read-write ``read`` lets an ``O_RDONLY`` fd run it
+``suggested-reply-length`` 64K the buffer a caller starts a query with
+``optional`` false see `Optional operations`_
+``availability`` fixed see `Dynamic availability`_
+``pre-post`` false see `Hooks`_
+=========================== ========== ========================================
+
+An op that reads is named ``<object>-get``, the object in the singular, and
+each kind of object has one. A get over a collection, as ``item-get`` is, is a
+query even while it returns one object, as an op's type never changes; its
+request fields are optional filters, none meaning every object. A filter
+matching nothing gives an empty answer, and an ID naming nothing fails with
+``ENOENT``. An action get returns exactly one thing, such as the device.
+
+What an op takes or returns is a record: the fields one level of a message
+may hold, one per line. ``name?`` is an optional field, ``name[N]`` one that
+occurs up to N times, ``name[enum]`` one that occurs up to once per value of
+the enum, and ``name[]`` one a query's reply repeats any number of times. A
+field with fields indented below it is a record, the one declared there, as
+``items[]`` is. Each field has generated helpers named after its op and the
+records it is in: ``ctlv_example_item_set_index_get()`` reads ``index`` in an
+``item-set`` request, and ``ctlv_example_item_get_items_index_put()`` writes
+it in an item of the ``item-get`` reply.
+
+Fields that several messages repeat can be declared once as a named record,
+and then the specification reads:
+
+.. code-block:: yaml
+
+ records:
+ # One item, as item-set takes it and item-get lists it.
+ item:
+ # Which item it is.
+ index: uint(..4095)
+ # What the item is called.
+ label?: string(..63)
+
+ ops:
+ # Sets an item's label and data.
+ item-set:
+ type: action
+ request:
+ $include: item
+ # The item's contents, which the device reads.
+ data?: blob(in, ..1M, align 8, va)
+
+ # Lists the items.
+ item-get:
+ type: query
+ reply:
+ # Every item the device keeps.
+ items[]: record item
+
+``$include: item`` puts the record's fields in place, and ``record item`` makes
+it a whole message or a record's field. The field is then the same wherever it
+appears: ``item.index`` has one type, one bound and one generated helper in
+the ``item-set`` request and in the items ``item-get`` returns, and
+``ctlv_example_item_index_put()`` writes it in the latter. This guide keeps
+the fields spelled out.
+
+A field's type, the ID it has in a message, is made of the family's number,
+its record and its place in that record. A named record is numbered by its
+place in ``records``, an op's request and reply by the op's place in ``ops``,
+and a record declared in place by its place among the records declared beside
+it. Every one of those places is ABI, so records, fields and ops are only ever
+appended. Records declared in place go at most four deep and fifteen side by
+side below a named record or an op's message; a deeper or wider tree takes
+another named record.
+
+A field has one schema kind. Bounds follow it in parentheses, inclusive, with
+limits such as ``u32-max`` written by name and sizes such as ``4K``, which an
+integer takes only with the ``bytes`` unit, as in ``uint(1..4K, bytes)``:
+
+============ ============================== ==================================
+Kind Holds Declared as, for example
+============ ============================== ==================================
+``uint`` an unsigned 64-bit integer ``uint(..4095)``
+``sint`` a signed 64-bit integer ``sint(-8..8)``
+``bool`` true or false ``bool``
+``string`` text, bounded without its NUL ``string(..63)``
+``binary`` bytes ``binary(16..64)``
+enum one value of an enum the enum's name
+flags a set of flags the flags' name
+record fields ``record item``, or fields below
+``blob`` caller memory, see `Blobs`_ ``blob(in, ..1M, align 8, va)``
+============ ============================== ==================================
+
+Bounds, record membership and whether a field is required are ABI.
+
+The top-level keys may come in any order; the in-tree specifications put
+``enums`` and ``flags`` first, then ``records`` and ``ops``.
+
+An enum a uapi header already defines names the header in ``$header``, a path
+under ``include/uapi``, and the enum in ``$enum``. The enum's constants are its
+values, under their own names and numbers, whatever they start from and
+whatever gaps they leave, and need no comments. ``$end`` names a sentinel,
+leaving it and every constant after it out:
+
+.. code-block:: yaml
+
+ enums:
+ # The kernel's hash algorithms.
+ hash-algo:
+ $header: linux/hash_info.h
+ $enum: hash_algo
+ $end: HASH_ALGO__LAST
+
+The generated code includes the header rather than repeating the enum. CTLV
+takes any number from the lowest value up to one below ``$end``, or to the
+highest value without it, so a number in a gap reaches the callback, which
+refuses it. ``$end`` may move later as the header grows, never earlier. An
+enum written as ``extension`` has no values of its own; see
+`Inheritance and HW driver fields`_.
+
+No message outgrows the core's inline limit, ``CTLV_MAX_INLINE_MESSAGE_LEN``
+(64 KiB): the generator refuses an operation whose request or action reply
+could be longer, or a query whose longest field could not fit in one
+fragment. Larger data goes in a blob.
+
+Generate the family
+===================
+
+A new family is one change holding its specification, its registry entry and
+its output manifest entry::
+
+ Documentation/ctlv/specs/ctlv-example.yaml
+ Documentation/ctlv/families.yaml
+ Documentation/ctlv/generated-files.yaml
+
+The family's position in ``families.yaml`` is its family ID, part of every
+type and op ID it generates. Append only: never reorder, remove,
+rename or reuse an entry. Agree the position with the CTLV maintainers (CTLV
+SUBSYSTEM in MAINTAINERS) before adding a family, since families added in
+parallel trees would otherwise end up with the same ID. The output manifest
+says where the generated UAPI, kernel and userspace files go.
+
+Generate the files and record the new family's ABI::
+
+ make -C tools/ctlv regen
+
+``regen`` names each file it writes. A new family should add its generated
+files, its snapshot, ``Documentation/ctlv/abi/ctlv-example.yaml``, its entry
+in ``Documentation/ctlv/abi/families.registry.yaml`` and one line in
+``include/uapi/ctlv/ctlv_families_gen.h``, and change nothing generated for
+another family. Anything else means the specification or the generator
+changed more than intended. ``regen`` refuses a specification change that
+breaks the ABI as last committed unless ``ABI_BREAK=1`` says it is meant, so a
+snapshot's diff shows every ABI change for review. ``make -C tools/ctlv``
+checks the same without writing anything.
+
+Never edit a generated file; change the YAML or the generator and regenerate.
+
+Create and destroy a device
+===========================
+
+CTLV comes in two parts. The engine in ``lib/ctlv/`` is what a subsystem embeds
+in its devices and calls from their file operations. The nodes, declared in
+``<linux/ctlv_device.h>``, are character devices of the subsystem's own class,
+with the engine's file operations.
+
+A device's CTLV state is a ``struct ctlv``, embedded in the structure of
+whoever gives the device. A callback gets the instance and reaches that
+structure with ``container_of()``. The node is a ``struct ctlv_device``, which
+the structure embeds beside the instance:
+
+.. code-block:: c
+
+ struct example_device {
+ struct ctlv_device ctlvdev;
+ struct ctlv ctlv;
+ u64 items_count;
+ u64 generation;
+ };
+
+ static struct example_device *to_example(struct ctlv *ctlv)
+ {
+ return container_of(ctlv, struct example_device, ctlv);
+ }
+
+The subsystem's class is a ``struct ctlv_device_class``, registered once with
+``ctlv_device_class_register()``, which takes its device numbers. Its
+``dev_release`` frees the structure, so an open node holds the module that
+registered the class:
+
+.. code-block:: c
+
+ static void example_release(struct device *dev)
+ {
+ struct example_device *example =
+ container_of(dev, struct example_device, ctlvdev.dev);
+
+ ctlv_fini(&example->ctlv);
+ ctlv_device_fini(&example->ctlvdev);
+ kfree(example);
+ }
+
+ static struct ctlv_device_class example_class = {
+ .class = {
+ .name = "example",
+ .dev_release = example_release,
+ },
+ .max_devices = 16,
+ };
+
+A device of the class is then made:
+
+.. code-block:: c
+
+ example = kzalloc(sizeof(*example), GFP_KERNEL);
+ if (!example)
+ return -ENOMEM;
+ ret = ctlv_device_init(&example->ctlvdev, &example_class, parent,
+ &example->ctlv);
+ if (!ret)
+ ret = ctlv_init(&example->ctlv, &example->ctlvdev.dev,
+ &ctlv_example_family, NULL);
+ if (ret)
+ goto err_put;
+ ctlv_publish(&example->ctlv);
+ ret = ctlv_device_add(&example->ctlvdev);
+ if (ret)
+ goto err_unpublish;
+
+``ctlv_device_init()`` names the node after the class, ``/dev/example0``.
+``ctlv_init()`` resolves the family chain. Until the instance is published, the
+driver selects its optional operations and their initial dynamic state, see
+Hooks and availability. ``ctlv_publish()`` makes operations live, and only then
+may the node be added, as a program may open it as soon as it appears.
+``ctlv_device_add()`` adds the node, whose uevent names the family; that is how
+udev finds the device. Destruction is the reverse:
+
+.. code-block:: c
+
+ ctlv_device_del(&example->ctlvdev);
+ ctlv_unpublish(&example->ctlv);
+ put_device(&example->ctlvdev.dev);
+
+``ctlv_unpublish()`` waits for the operations in flight; no callback runs
+afterwards. Each open file description fails its operations with ``-ENODEV``.
+The structure is freed with the last reference, the device's or an open file's,
+so what the callbacks used may stay in it. From ``ctlv_device_init()`` on,
+``put_device()`` alone unwinds a failed initialization, as it does a failed add
+once the instance is unpublished.
+
+A file of another subsystem
+---------------------------
+
+A subsystem may serve the instance on a file of its own instead, a misc device
+whose other operations are the subsystem's, say. It publishes the instance
+around the life of that file, attaches CTLV to each open file and passes on its
+ioctls:
+
+.. code-block:: c
+
+ static int foo_open(struct inode *inode, struct file *filp)
+ {
+ struct foo *foo = container_of(filp->private_data, struct foo, misc);
+ struct ctlv_file *cfile = ctlv_file_open(&foo->ctlv, filp);
+
+ if (IS_ERR(cfile))
+ return PTR_ERR(cfile);
+ filp->private_data = cfile;
+ return 0;
+ }
+
+ static long foo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
+ {
+ long ret = ctlv_file_ioctl(filp->private_data, cmd, arg);
+
+ return ret == -ENOIOCTLCMD ? foo_own_ioctl(filp, cmd, arg) : ret;
+ }
+
+Its release calls ``ctlv_file_close()``. The file's uevent is the other
+subsystem's and names no family, so a program opens the file by its name.
+
+The ``struct ctlv_hooks`` passed to ``ctlv_init()`` holds the device's pre and
+post hooks, see `Hooks`_. The device structure's release calls ``ctlv_fini()``
+when the last reference goes.
+
+Implement operations
+====================
+
+Action
+------
+
+Generated getters read only what validation accepted, and generated
+serializers write only what the reply declares:
+
+.. code-block:: c
+
+ int ctlv_example_item_set_action(struct ctlv *ctlv,
+ struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+ {
+ struct example_device *example = to_example(ctlv);
+ u64 index = ctlv_example_item_set_index_get(request);
+
+ if (item_busy(example, index))
+ return ctlv_op_error(ctx, -EBUSY, "item is busy");
+ return item_set(example, index);
+ }
+
+Use ``_present()`` for optional fields and ``_for_each()`` for repeated
+ones. ``ctlv_op_error()`` preserves the errno and adds a short diagnostic when
+the caller supplied enough reply space.
+
+Query
+-----
+
+.. code-block:: c
+
+ int ctlv_example_item_get_query(struct ctlv *ctlv,
+ struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+ {
+ struct example_device *example = to_example(ctlv);
+ u64 generation = READ_ONCE(example->generation);
+ u64 cursor = ctlv_op_query_cursor(ctx);
+ struct ctlv_cursor item;
+ u64 i = cursor ? cursor - 1 : 0;
+
+ if (cursor && ctlv_op_query_generation(ctx) != generation)
+ return -ESTALE;
+ for (; i < example->items_count; i++) {
+ if (!ctlv_example_item_get_items_start(reply, &item))
+ return ctlv_op_query_more(ctx, generation, i + 1);
+ if (!ctlv_example_item_get_items_index_put(&item, i)) {
+ ctlv_record_cancel(&item);
+ return ctlv_op_query_more(ctx, generation, i + 1);
+ }
+ ctlv_record_end(&item);
+ }
+ return ctlv_op_query_done(ctx, generation);
+ }
+
+A continuation cursor is nonzero and advances. Every continuing fragment
+completes a logical leaf. An item that runs out of room partway is dropped with
+``ctlv_record_cancel()``, for the next fragment to write again. Call
+``ctlv_op_query_more()`` while cursors for open continuation records are alive.
+A generation mismatch returns ``-ESTALE``.
+
+Hooks and availability
+======================
+
+Hooks
+-----
+
+The pre and post hooks of the ``struct ctlv_hooks`` passed to ``ctlv_init()``
+run around every action and query the core does not answer. An op declared
+with ``pre-post: true`` also runs hooks of its own family, inside the
+device's:
+
+.. code-block:: yaml
+
+ item-get:
+ type: query
+ pre-post: true
+
+The family then implements ``ctlv_example_item_get_pre()`` and
+``ctlv_example_item_get_post()``, and the order is::
+
+ device pre -> operation pre -> availability check -> callback
+ -> operation post -> device post
+
+Each successful pre hook has one post hook. A failing pre hook undoes its own
+work. Hooks may sleep and must not retain ``ctx``. Each hook and callback gets
+the instance.
+
+Optional operations
+-------------------
+
+An op declared ``optional: true`` is published only on a device whose driver
+provides it before publishing the instance:
+
+.. code-block:: yaml
+
+ item-set:
+ type: action
+ optional: true
+
+.. code-block:: c
+
+ ctlv_example_item_set_provide(&example->ctlv);
+
+On a device that does not provide it, a program finds no entry for the
+operation in ``op-get`` or ``op-schema-get``, and executing it fails with
+``-EOPNOTSUPP``.
+
+Dynamic availability
+--------------------
+
+An op declared ``availability: dynamic`` can be refused for a while on a
+device that publishes it:
+
+.. code-block:: yaml
+
+ item-get:
+ type: query
+ availability: dynamic
+
+.. code-block:: c
+
+ ctlv_example_item_get_disable(&example->ctlv);
+ ctlv_example_item_get_enable(&example->ctlv);
+
+A disabled operation stays in ``op-get``, which reports it disabled, and
+returns ``-EOPNOTSUPP``.
+
+Blobs
+=====
+
+A blob field describes caller-owned memory. Resolve it only in the callback
+that received its request and release every successful resolve before return:
+
+.. code-block:: c
+
+ blob = ctlv_example_item_set_data_resolve(ctx, request);
+ if (IS_ERR(blob))
+ return PTR_ERR(blob);
+
+ ret = ctlv_blob_read(blob, 0, buf, ctlv_blob_len(blob));
+ put_ret = ctlv_blob_put(blob);
+ return ret ?: put_ret;
+
+The schema fixes direction, backing, length, and alignment. Resolution holds
+the backing for the ioctl. An output blob too short for
+what the operation produces is the caller's to enlarge: return
+``ctlv_blob_too_short()`` with the length needed, rather than writing part of
+it.
+
+Only a request carries a blob; the generator refuses one in a reply. A request
+that shares fields with a reply includes them and declares its blob beside them,
+as ``item-set`` does when it includes ``item``.
+
+Inheritance and HW driver fields
+================================
+
+Several HW drivers can implement ``ctlv-example``, and each knows things
+about its devices that a program may want, such as which bank of the foo
+chip's SRAM holds the items. Those do not belong in ``ctlv-example``, which
+all of them share. Instead, ``ctlv-example`` leaves a place for them, an
+extension point, and each HW driver has a family of its own that inherits
+``ctlv-example`` and fills it.
+
+Say ``ctlv-example`` gains a system call reporting the device::
+
+ device_get_action() -> { details? };
+
+On a device of the foo driver, ``details`` holds foo's fields::
+
+ device_get_action() -> { details? { u64 sram_bank; }; };
+
+``ctlv-example`` declares the op and leaves the contents of ``details`` to the
+HW driver, with an extension point:
+
+.. code-block:: yaml
+
+ ops:
+ # Reports the device.
+ device-get:
+ type: action
+ fd-access: read
+ reply:
+ # Empty here; a HW driver's family fills it with fields of its own.
+ details?: extension device-details
+
+The foo driver's family fills it by declaring a record of the same name:
+
+.. code-block:: yaml
+
+ # The devices of the foo driver, which fill what ctlv-example leaves open.
+ family: ctlv-example-foo
+ inherits: ctlv-example
+
+ records:
+ # The extension point ctlv-example left empty, filled for foo devices.
+ device-details:
+ # The bank of the foo chip's SRAM that holds the items.
+ sram-bank: uint(..3)
+
+The foo driver initializes its instances with ``ctlv_example_foo_family``, and
+they answer every ``ctlv-example`` op. A family implements only the ops it
+declares, so the inherited ones keep the callbacks and hooks of
+``ctlv-example``, and the foo module depends on the module that implements
+``ctlv-example``.
+
+The ``device-get`` callback in ``ctlv-example`` opens the record and has the
+device's family fill it:
+
+.. code-block:: c
+
+ if (ctlv_example_device_get_details_start(reply, &details)) {
+ ret = ctlv_ext_fill(ctx, NULL, &details);
+ ctlv_record_end(&details);
+ if (ret)
+ return ret;
+ }
+
+The foo family implements the generated fill callback:
+
+.. code-block:: c
+
+ int ctlv_example_foo_device_details_fill(struct ctlv *ctlv,
+ struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *record)
+ {
+ struct foo_device *foo = container_of(ctlv, struct foo_device,
+ example.ctlv);
+
+ ctlv_example_foo_device_details_sram_bank_put(record,
+ foo->sram_bank);
+ return 0;
+ }
+
+``request`` is the matching record of the request, for an op whose request has
+an extension point too; ``device-get`` has none and passes ``NULL``. An
+extension point no family fills adds no record and no length. ``ctlv-example``
+never names ``ctlv-example-foo``, and the foo family names nothing of
+``ctlv-example`` beyond the extension point it fills.
+
+A program knows whose fields it is reading without looking at them. A
+device's family chain runs from the root to the family of its instance,
+here ``ctlv``, ``ctlv-example`` and ``ctlv-example-foo``, and that last one,
+the leaf, is the HW driver's. ``family-get`` reports the chain on the open
+device, so a program picks its handler for the HW driver's fields once, when
+it opens the device; the uevent carries the same chain for udev rules. A tool
+that knows only ``ctlv-example`` skips the TLVs it does not know, as on any
+reply. Each TLV in a filled record also carries the family that declared its
+field, in bits 63..48 of its type: ``CTLV_TYPE_FAMILY()`` extracts it and
+``family-get`` maps it to a name.
+
+An enum can be left open the same way, when each HW driver has values of its
+own, such as the data formats its devices produce. ``ctlv-example`` declares
+the enum as an enum extension point, and its fields name it as any enum:
+
+.. code-block:: yaml
+
+ enums:
+ # The format of a device's data; a HW driver's family gives the values.
+ data-format: extension
+
+The foo driver's family gives the values, by declaring an enum of that name:
+
+.. code-block:: yaml
+
+ enums:
+ # Fills ctlv-example's data-format.
+ data-format:
+ # The foo device's own layout.
+ - foo-raw
+
+One family of a chain gives the values, numbered from 1 and appended to as any
+enum's. The core takes only those values on a device of that family, in a
+request and in what its driver puts in a reply, and none on a device whose
+chain gives none. The names are in the uapi header of the family that gives
+them, such as ``CTLV_EXAMPLE_FOO_DATA_FORMAT_FOO_RAW``. A family that checks
+values a HW driver hands it, before any reaches a reply, asks
+``ctlv_enum_ext_max()`` for the highest the driver's family gives.
+
+The structures embed one another as the families inherit. The module
+implementing ``ctlv-example`` keeps what every device of that family has in a
+structure around the instance, and exports what initializes it for a family
+that inherits ``ctlv-example``. A HW driver embeds that structure in its own
+and describes itself there, for the ``ctlv-example`` callbacks to use:
+
+.. code-block:: c
+
+ struct ctlv_example_device {
+ struct ctlv ctlv;
+ const struct ctlv_example_provider *provider;
+ };
+
+ struct foo_device {
+ struct ctlv_device ctlvdev;
+ struct ctlv_example_device example;
+ u32 sram_bank;
+ };
+
+ static const struct ctlv_example_provider foo_provider = {
+ .name = "foo",
+ .ops = &foo_provider_ops,
+ };
+
+ foo = kzalloc(sizeof(*foo), GFP_KERNEL);
+ if (!foo)
+ return -ENOMEM;
+ ret = ctlv_device_init(&foo->ctlvdev, &foo_class, parent,
+ &foo->example.ctlv);
+ if (!ret)
+ ret = ctlv_example_device_init(&foo->example, &foo->ctlvdev.dev,
+ &ctlv_example_foo_family,
+ &foo_provider);
+
+The node is of the foo driver's own class, ``foo_class``, registered as
+``example_class`` is above. ``ctlv_example_device_init()`` refuses a family
+that ``ctlv_family_inherits()`` says is not ``ctlv-example`` or a descendant,
+then calls ``ctlv_init()``.
+
+Reporting an error
+------------------
+
+A callback returns a negative errno, and that is what the ioctl returns. Where
+CTLV itself refuses a message it also writes error information into the
+caller's reply buffer, naming the reason and the offset of the offending
+TLV. A family implementation does not build that; it adds its own errno
+and its reply.
+
+Configuration
+=============
+
+``CONFIG_CTLV``
+ CTLV. A family implementation selects it.
+
+Kernel API
+==========
+
+.. kernel-doc:: include/linux/ctlv.h
+.. kernel-doc:: lib/ctlv/core.c
+ :export:
+.. kernel-doc:: lib/ctlv/builder.c
+ :export:
+.. kernel-doc:: lib/ctlv/schema.c
+ :export:
+.. kernel-doc:: lib/ctlv/blob.c
+ :export:
+.. kernel-doc:: include/linux/ctlv_device.h
+.. kernel-doc:: lib/ctlv/device.c
+ :export:
diff --git a/Documentation/driver-api/index.rst b/Documentation/driver-api/index.rst
index aaf5d76ed074..0c5b9d2165d8 100644
--- a/Documentation/driver-api/index.rst
+++ b/Documentation/driver-api/index.rst
@@ -37,6 +37,7 @@ of interest to most developers working on device drivers.
early-userspace/index
connector
+ ctlv
device-io
devfreq
dma-buf
diff --git a/MAINTAINERS b/MAINTAINERS
index 29bdb4de915d..b16bedbbe0b6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7188,6 +7188,7 @@ M: Jiri Pirko <jiri@resnulli.us>
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/ctlv/
+F: Documentation/driver-api/ctlv.rst
F: Documentation/userspace-api/ctlv.rst
F: include/linux/ctlv*.h
F: include/uapi/ctlv/
--
2.56.0
next prev parent reply other threads:[~2026-11-13 15:56 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 02/15] ctlv: Add the UAPI message format and family API Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 03/15] ctlv: Add the root family specification and generated files Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 04/15] Documentation: ctlv: Add userspace ABI Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 05/15] ctlv: Prepare message input handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 06/15] ctlv: Prepare blob descriptor validation Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 07/15] ctlv: Prepare schema validation Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 08/15] ctlv: Prepare message output handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 11/15] ctlv: Add operation dispatch Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 12/15] ctlv: Add character device nodes Jiri Pirko
2026-11-13 15:55 ` Jiri Pirko [this message]
2026-11-13 15:55 ` [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl Jiri Pirko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261113155515.2510194-14-jiri@resnulli.us \
--to=jiri@resnulli.us \
--cc=James.Bottomley@HansenPartnership.com \
--cc=aik@amd.com \
--cc=alistair.francis@wdc.com \
--cc=aneesh.kumar@kernel.org \
--cc=ankita@nvidia.com \
--cc=cedric.xing@intel.com \
--cc=dave.hansen@linux.intel.com \
--cc=dave.jiang@intel.com \
--cc=david.quigley@intel.com \
--cc=drdeeglaze@gmail.com \
--cc=elena.reshetova@intel.com \
--cc=jarkko@kernel.org \
--cc=jgg@ziepe.ca \
--cc=jic23@kernel.org \
--cc=jpiotrowski@nvidia.com \
--cc=kas@kernel.org \
--cc=ksinyuk@kernel.org \
--cc=kvm@vger.kernel.org \
--cc=linux-api@vger.kernel.org \
--cc=linux-coco@lists.linux.dev \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=lukas@wunner.de \
--cc=nik.borisov@suse.com \
--cc=peter.fang@intel.com \
--cc=pgonda@google.com \
--cc=qinkun@google.com \
--cc=rick.p.edgecombe@intel.com \
--cc=rodrigo.vivi@intel.com \
--cc=sameo@meta.com \
--cc=sathyanarayanan.kuppuswamy@linux.intel.com \
--cc=sergii.dmytruk@3mdeb.com \
--cc=suzuki.poulose@arm.com \
--cc=x86@kernel.org \
--cc=yeoreum.yun@arm.com \
--cc=zohar@linux.ibm.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®