5 Raw FFI Layer
dxtr edited this page 2026-10-18 16:58:25 +02:00

Raw FFI Layer

cl-vk/ffi is the Vulkan C API transcribed into Lisp, generated from the registry: every type, constant and command of Vulkan 1.0 to 1.4 and of every extension, except those for platforms other than Wayland, X11 (xcb, Xlib, Xlib-XRandR) and Windows, and provisional ones. That is about 700 commands and 1,500 structs and unions.

It does no error checking beyond argument types and no memory management. Use it for what the idiomatic layer does not cover yet, usually on cl-vk's objects; Recipes shows how the two mix.

(asdf:load-system :cl-vk/ffi)   ; or :cl-vk, which loads it

(defpackage #:example
  (:use #:cl)
  (:local-nicknames (#:vk-ffi #:cl-vk/ffi)))
(in-package #:example)

(let ((global (vk-ffi:load-global-commands)))
  (cl-ffi:with-foreign-object (version :uint32)
    (vk-ffi:vk-enumerate-instance-version global version)
    (let ((version (cl-ffi:mem-ref version :uint32)))
      (list (vk-ffi:vk-api-version-major version)
            (vk-ffi:vk-api-version-minor version)))))
;; => (1 4)

Names

C Lisp
vkCreateInstance function vk-create-instance
VkApplicationInfo struct vk-application-info, as (:struct vk-application-info)
pNext, a member slot :p-next
VK_SUCCESS, VK_WHOLE_SIZE constants +vk-success+, +vk-whole-size+
VK_API_VERSION_1_3 constant +vk-api-version-1-3+
VkResult, VkQueueFlags foreign types vk-result, vk-queue-flags
PFN_vkAllocationFunction foreign type pfn-vk-allocation-function
VK_MAKE_API_VERSION function vk-make-api-version

Words of camel-case names are separated with hyphens; runs of capitals and digits stay with their word (VkPhysicalDeviceVulkan13Features is vk-physical-device-vulkan13-features, VkExtent2D is vk-extent2d).

The package uses no other package, not even common-lisp, so that it can define names such as type and format of its own. Refer to it through a package-local nickname. Slots are keywords, so bind them with explicit pairs:

(cl-ffi:with-foreign-slots ((s-type :s-type) (p-next :p-next)) info
    (:struct vk-ffi:vk-application-info)
  ...)

Command tables

Commands are called through function pointers, never through the loader's exported symbols. A command table holds the pointers of one level of the API, and every command takes its table first:

Function Table Commands
load-global-commands global-commands vkCreateInstance, vkEnumerateInstanceVersion, vkEnumerateInstanceExtensionProperties, vkEnumerateInstanceLayerProperties
load-instance-commands instance instance-commands those of a VkInstance and its physical devices
load-device-commands instance device device-commands those of a VkDevice, its queues and command buffers

The device table's pointers come from vkGetDeviceProcAddr, so calls go straight to the driver.

With the idiomatic layer, you never load tables: vk:instance-commands and vk:device-commands return those of cl-vk's objects, and signal vk:destroyed-object-error once the object is destroyed.

instance-commands-instance and device-commands-device return the handle a table was loaded for. command-table is the type of all three.

Tables never change once loaded, and may be shared between threads. Their pointers belong to the process that loaded them: a program that keeps a table in an image it saves loads it again when the image starts, as cl-vk does for the global table it keeps, through cl-ffi:add-image-start-hook.

Missing commands

A table has no pointer for a command the instance or device does not provide: one of an API version or extension that was not enabled, or that the driver lacks. Calling it signals command-unavailable, whose command-unavailable-name is the command's C name:

vkCmdDrawMeshTasksEXT is not available: the device did not provide it.  Enable
the API version or extension that provides it when creating the device.

command-available-p asks first, by C name, aliases included:

(vk-ffi:command-available-p (vk:device-commands device) "vkCmdDrawMeshTasksEXT")
;; => NIL

Commands

Each command is a function that takes the table, then the C parameters in order:

(vk-ffi:vk-create-fence (vk:device-commands device) (vk:raw-handle device)
                        info (cl-ffi:null-pointer) fence-pointer)
  • Values are passed as they are in C: integers, floats and foreign pointers. Enums and flags are integers; nothing is translated.
  • Dispatchable handles (VkInstance, VkPhysicalDevice, VkDevice, VkQueue, VkCommandBuffer) are foreign pointers; the others are 64-bit integers.
  • The result is returned as is: a VkResult integer, to give to vk:check, or whatever else the command returns.
  • Each argument's type is checked, so a wrong one signals a type-error rather than crashing.
  • Commands are inline, and a call allocates nothing when its pointers are on the stack or preallocated.

Each command's docstring gives its C prototype, its success codes and its error codes:

(documentation 'vk-ffi:vk-create-instance 'function)
;; => "Call vkCreateInstance through COMMANDS, a GLOBAL-COMMANDS table:
;;
;;     VkResult vkCreateInstance(
;;         const VkInstanceCreateInfo* pCreateInfo,
;;         const VkAllocationCallbacks* pAllocator,
;;         VkInstance* pInstance)
;;
;;     Success codes: VK_SUCCESS.
;;
;;     Error codes: VK_ERROR_OUT_OF_HOST_MEMORY, ..."

Structs

Structs and unions are cl-ffi structs, (:struct vk-ffi:vk-application-info). Their layouts are checked against the C compiler's, member by member, by a test (see Generator and Registry).

structure-type returns the value a struct's sType must hold, or nil if it has none:

(vk-ffi:structure-type 'vk-ffi:vk-fence-create-info)
;; => 8   ; VK_STRUCTURE_TYPE_FENCE_CREATE_INFO

The idiomatic layer's with-structs, set-struct and struct-ref work on these same structs and are far easier; see Structs.

Bit-fields

cl-ffi has no bit-fields, so each storage unit of C bit-fields is one integer slot, :bit-field-0 and so on. bit-field and (setf bit-field) read and write a field by name:

(setf (vk-ffi:bit-field instance 'vk-ffi:vk-acceleration-structure-instance-khr :mask) #xff)
(vk-ffi:bit-field instance 'vk-ffi:vk-acceleration-structure-instance-khr :mask)
;; => 255

(setf bit-field) signals a type-error for a value that does not fit, expecting (unsigned-byte width). Both signal a type-error for a struct without bit-fields or a field it has not, its expected type listing the valid ones. The struct macros of the idiomatic layer take bit-fields by name too.

Versions

The registry's version macros are functions:

Function C macro
vk-make-api-version variant major minor patch VK_MAKE_API_VERSION
vk-api-version-variant, -major, -minor, -patch version VK_API_VERSION_VARIANT and so on
vk-make-version, vk-version-major, -minor, -patch the deprecated VK_MAKE_VERSION and so on
vk-make-video-std-version major minor patch VK_MAKE_VIDEO_STD_VERSION
(= (vk-ffi:vk-make-api-version 0 1 3 0) vk-ffi:+vk-api-version-1-3+)
;; => T

The loader

libvulkan is the cl-ffi foreign library for the Vulkan loader: libvulkan.so.1 (or libvulkan.so) on Unix, vulkan-1.dll on Windows. It is loaded when cl-vk/ffi is. Only vkGetInstanceProcAddr is looked up by symbol; every other command comes from it.

Packages

cl-vk/ffi is generated, in registry/vulkan.lisp. The hand-written runtime under it is cl-vk/ffi-support: the loader, the command tables, the version functions and the macros the generated definitions expand into (define-constant, define-ctype, define-enum, define-struct, define-struct-alias, define-command, define-command-names). cl-vk/ffi re-exports everything a user needs from it, so use cl-vk/ffi.