Implement the REMOTE_SESSION_CREATE and INIT_RELEASE FastRPC operations, which establish and tear down a user process on the DSP.
DRM_IOCTL_QDA_REMOTE_SESSION_CREATE (drm_qda_init_create) Creates a new process on the DSP by sending an INIT_CREATE message via the FastRPC INIT_HANDLE. The caller provides the ELF image as a GEM handle along with optional process attributes. A GEM buffer is allocated per session to hold the DSP process image; it is cached in qda_file_priv and reused for the lifetime of the session. If attrs is non-zero the INIT_CREATE_ATTR message is used instead of INIT_CREATE. Both create the protection domain and load the process image; INIT_CREATE_ATTR additionally carries the process attributes and signature length, which the DSP applies to the domain it creates. Issuing INIT_CREATE again on the same file without an intervening INIT_RELEASE replaces the process image, so the initialisation memory cached for the previous attempt is released first. INIT_RELEASE Tears the DSP process down. It is issued from qda_release_dsp_process() on postclose, guarded by drm_dev_enter() so the message is only sent while the device is still accessible. Once the DSP has torn down the protection domain the initialisation memory is released. The initialisation memory is referenced both by the invocation context and by the file private data that caches it across invocations, so each takes a reference of its own and drops it independently. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Ekansh Gupta <[email protected]> --- Changes in v2: - Explain how INIT_CREATE_ATTR differs from INIT_CREATE (Dmitry Baryshkov) - Take a separate reference on the process initialisation memory for the file private data and the invocation context, so it is no longer leaked when process creation fails - Document why a second INIT_CREATE releases the previous initialisation memory --- drivers/accel/qda/qda_drv.c | 12 +++ drivers/accel/qda/qda_drv.h | 5 + drivers/accel/qda/qda_fastrpc.c | 213 ++++++++++++++++++++++++++++++++++++++++ drivers/accel/qda/qda_fastrpc.h | 35 +++++++ drivers/accel/qda/qda_ioctl.c | 77 +++++++++++++++ drivers/accel/qda/qda_ioctl.h | 1 + include/uapi/drm/qda_accel.h | 31 +++++- 7 files changed, 373 insertions(+), 1 deletion(-) diff --git a/drivers/accel/qda/qda_drv.c b/drivers/accel/qda/qda_drv.c index f925bbcfa6e9..4c3a37cbf49e 100644 --- a/drivers/accel/qda/qda_drv.c +++ b/drivers/accel/qda/qda_drv.c @@ -12,6 +12,7 @@ #include <drm/qda_accel.h> #include "qda_drv.h" +#include "qda_gem.h" #include "qda_ioctl.h" #include "qda_prime.h" @@ -35,6 +36,16 @@ static int qda_open(struct drm_device *dev, struct drm_file *file) static void qda_postclose(struct drm_device *dev, struct drm_file *file) { struct qda_file_priv *qda_file_priv = file->driver_priv; + int idx; + + /* Only send the DSP release message while the device is accessible */ + if (drm_dev_enter(dev, &idx)) { + qda_release_dsp_process(qda_file_priv->qda_dev, file); + drm_dev_exit(idx); + } + + if (qda_file_priv->init_mem_gem_obj) + drm_gem_object_put(&qda_file_priv->init_mem_gem_obj->base); if (qda_file_priv->assigned_iommu_dev) qda_memory_manager_release_device(qda_file_priv->assigned_iommu_dev); @@ -49,6 +60,7 @@ static const struct drm_ioctl_desc qda_ioctls[] = { DRM_IOCTL_DEF_DRV(QDA_QUERY, qda_ioctl_query, 0), DRM_IOCTL_DEF_DRV(QDA_GEM_CREATE, qda_ioctl_gem_create, 0), DRM_IOCTL_DEF_DRV(QDA_GEM_MMAP_OFFSET, qda_ioctl_gem_mmap_offset, 0), + DRM_IOCTL_DEF_DRV(QDA_REMOTE_SESSION_CREATE, qda_ioctl_init_create, 0), DRM_IOCTL_DEF_DRV(QDA_REMOTE_INVOKE, qda_ioctl_invoke, 0), }; diff --git a/drivers/accel/qda/qda_drv.h b/drivers/accel/qda/qda_drv.h index 8332108c362f..4337894ff22d 100644 --- a/drivers/accel/qda/qda_drv.h +++ b/drivers/accel/qda/qda_drv.h @@ -28,6 +28,8 @@ struct qda_file_priv { struct qda_dev *qda_dev; /** @assigned_iommu_dev: IOMMU device assigned to this process */ struct qda_iommu_device *assigned_iommu_dev; + /** @init_mem_gem_obj: GEM object for PD initialization memory */ + struct qda_gem_obj *init_mem_gem_obj; /** @pid: Process ID for tracking */ pid_t pid; /** @remote_session_id: Unique session identifier */ @@ -82,4 +84,7 @@ int qda_init_device(struct qda_dev *qdev, int num_cbs); void qda_deinit_device(struct qda_dev *qdev); int qda_register_device(struct qda_dev *qdev); +/* DSP process / protection domain management */ +int qda_release_dsp_process(struct qda_dev *qdev, struct drm_file *file_priv); + #endif /* __QDA_DRV_H__ */ diff --git a/drivers/accel/qda/qda_fastrpc.c b/drivers/accel/qda/qda_fastrpc.c index 3c0ce5040500..2c72f86130aa 100644 --- a/drivers/accel/qda/qda_fastrpc.c +++ b/drivers/accel/qda/qda_fastrpc.c @@ -130,6 +130,9 @@ void qda_fastrpc_context_free(struct kref *ref) if (ctx->msg_gem_obj) drm_gem_object_put(&ctx->msg_gem_obj->base); + if (ctx->init_mem_gem_obj) + drm_gem_object_put(&ctx->init_mem_gem_obj->base); + kfree(ctx->args); kfree(ctx); } @@ -192,6 +195,45 @@ struct qda_fastrpc_invoke_ctx *qda_fastrpc_context_alloc(struct qda_dev *qdev) return ctx; } +/* + * Allocate a single DMA-coherent GEM object holding all kernel-internal + * buffers of one invocation, so the total DMA footprint is + * PAGE_ALIGN(sum of sizes) - typically a single page. + * + * The caller wires each argument slot with assign_kernel_gem_arg(), which + * takes a reference per slot, then drops the initial reference returned here. + */ +static struct qda_gem_obj *alloc_kernel_gem(struct qda_fastrpc_invoke_ctx *ctx, + size_t total_size) +{ + struct qda_file_priv *qda_priv = ctx->file_priv->driver_priv; + struct qda_dev *qdev = qda_priv->qda_dev; + struct qda_gem_obj *qda_gem_obj; + struct drm_gem_object *gem_obj; + + gem_obj = qda_gem_create_object(&qdev->drm_dev, qdev->iommu_mgr, + total_size, ctx->file_priv); + if (IS_ERR(gem_obj)) + return ERR_CAST(gem_obj); + + qda_gem_obj = to_qda_gem_obj(gem_obj); + /* Zero-initialise so OUT slots start clean */ + memset(qda_gem_obj->virt, 0, qda_gem_obj->size); + + return qda_gem_obj; +} + +static void assign_kernel_gem_arg(struct drm_qda_fastrpc_invoke_args *args, + struct drm_gem_object **gem_objs, int idx, + struct qda_gem_obj *kernel_gem, void *ptr, size_t len) +{ + args[idx].ptr = (u64)(uintptr_t)ptr; + args[idx].length = len; + args[idx].handle = 0; + drm_gem_object_get(&kernel_gem->base); + gem_objs[idx] = &kernel_gem->base; +} + /* * Resolve the physical address of an in/out buffer argument. The buffer is * backed either by a kernel-owned GEM object already recorded in @@ -368,6 +410,169 @@ int qda_fastrpc_invoke_pack(struct qda_fastrpc_invoke_ctx *ctx, struct qda_msg * return 0; } +/* + * INIT_RELEASE packed layout: + * [0] remote_session_id sizeof(int) IN + */ +static int qda_fastrpc_prepare_args_release_process(struct qda_fastrpc_invoke_ctx *ctx) +{ + struct drm_qda_fastrpc_invoke_args *args; + struct qda_gem_obj *kernel_gem; + u32 nscalars; + int err; + + ctx->sc = FASTRPC_SCALARS(FASTRPC_RMID_INIT_RELEASE, 1, 0); + ctx->handle = FASTRPC_INIT_HANDLE; + nscalars = REMOTE_SCALARS_LENGTH(ctx->sc); + + args = kzalloc_objs(*args, nscalars); + if (!args) + return -ENOMEM; + + ctx->gem_objs = kzalloc_objs(*ctx->gem_objs, nscalars); + if (!ctx->gem_objs) { + err = -ENOMEM; + goto err_free_args; + } + + kernel_gem = alloc_kernel_gem(ctx, sizeof(ctx->remote_session_id)); + if (IS_ERR(kernel_gem)) { + err = PTR_ERR(kernel_gem); + goto err_free_gem_objs; + } + + memcpy(kernel_gem->virt, &ctx->remote_session_id, + sizeof(ctx->remote_session_id)); + + assign_kernel_gem_arg(args, ctx->gem_objs, 0, kernel_gem, kernel_gem->virt, + sizeof(ctx->remote_session_id)); + drm_gem_object_put(&kernel_gem->base); + + ctx->args = args; + + return 0; + +err_free_gem_objs: + kfree(ctx->gem_objs); + ctx->gem_objs = NULL; +err_free_args: + kfree(args); + + return err; +} + +/* + * INIT_CREATE / INIT_CREATE_ATTR packed layout: + * [0] inbuf sizeof(fastrpc_create_process_inbuf) IN + * [1] comm TASK_COMM_LEN IN + * [2] ELF user-supplied GEM handle, not packed IN + * [3] pages sizeof(fastrpc_phy_page) IN + * [4] attrs sizeof(u32) IN + * [5] siglen sizeof(u32) IN + */ +static int qda_fastrpc_prepare_args_init_create(struct qda_fastrpc_invoke_ctx *ctx, void *argp) +{ + struct drm_qda_init_create *init = argp; + struct fastrpc_create_process_inbuf *inbuf; + struct drm_qda_fastrpc_invoke_args *args; + struct qda_gem_obj *kernel_gem; + u32 nscalars; + int err; + u8 *gv; + + const size_t off_inbuf = 0; + const size_t off_comm = off_inbuf + sizeof(*inbuf); + const size_t off_pages = off_comm + TASK_COMM_LEN; + const size_t off_attrs = off_pages + sizeof(struct fastrpc_phy_page); + const size_t off_siglen = off_attrs + sizeof(u32); + const size_t total_size = off_siglen + sizeof(u32); + + if (init->filelen > FASTRPC_INIT_FILELEN_MAX) + return -EINVAL; + + /* Reject an unusable ELF handle before involving the DSP */ + if (init->filelen && init->filehandle) { + struct drm_gem_object *file_gem_obj; + + err = get_gem_obj_from_handle(ctx, init->filehandle, &file_gem_obj); + if (err) + return err; + drm_gem_object_put(file_gem_obj); + } + + ctx->sc = init->attrs ? FASTRPC_SCALARS(FASTRPC_RMID_INIT_CREATE_ATTR, 4, 0) + : FASTRPC_SCALARS(FASTRPC_RMID_INIT_CREATE, 4, 0); + ctx->handle = FASTRPC_INIT_HANDLE; + nscalars = REMOTE_SCALARS_LENGTH(ctx->sc); + + args = kzalloc_objs(*args, FASTRPC_CREATE_PROCESS_NARGS); + if (!args) + return -ENOMEM; + + ctx->gem_objs = kzalloc_objs(*ctx->gem_objs, nscalars); + if (!ctx->gem_objs) { + err = -ENOMEM; + goto err_free_args; + } + + kernel_gem = alloc_kernel_gem(ctx, total_size); + if (IS_ERR(kernel_gem)) { + err = PTR_ERR(kernel_gem); + goto err_free_gem_objs; + } + gv = kernel_gem->virt; + + inbuf = (struct fastrpc_create_process_inbuf *)(gv + off_inbuf); + inbuf->remote_session_id = ctx->remote_session_id; + inbuf->namelen = strlen(current->comm) + 1; + inbuf->filelen = init->filelen; + inbuf->pageslen = 1; + inbuf->attrs = init->attrs; + inbuf->siglen = init->siglen; + + memcpy(gv + off_comm, current->comm, inbuf->namelen); + + ctx->input_pages = (struct fastrpc_phy_page *)(gv + off_pages); + setup_pages_from_gem_obj(ctx->init_mem_gem_obj, ctx->input_pages); + + *(u32 *)(gv + off_attrs) = inbuf->attrs; + *(u32 *)(gv + off_siglen) = inbuf->siglen; + + /* + * ctx->sc encodes 4 inbufs, so gem_objs[] only has 4 slots. args[4] + * and args[5] are packed for the DSP but must not be registered there, + * and are never walked because the argument loops stop at nscalars. + */ + assign_kernel_gem_arg(args, ctx->gem_objs, 0, kernel_gem, + gv + off_inbuf, sizeof(*inbuf)); + assign_kernel_gem_arg(args, ctx->gem_objs, 1, kernel_gem, + gv + off_comm, inbuf->namelen); + args[2].ptr = init->file; + args[2].length = inbuf->filelen; + args[2].handle = init->filehandle; + assign_kernel_gem_arg(args, ctx->gem_objs, 3, kernel_gem, + gv + off_pages, sizeof(struct fastrpc_phy_page)); + args[4].ptr = (u64)(uintptr_t)(gv + off_attrs); + args[4].length = sizeof(u32); + args[5].ptr = (u64)(uintptr_t)(gv + off_siglen); + args[5].length = sizeof(u32); + + drm_gem_object_put(&kernel_gem->base); + + ctx->args = args; + ctx->inbuf = inbuf; + + return 0; + +err_free_gem_objs: + kfree(ctx->gem_objs); + ctx->gem_objs = NULL; +err_free_args: + kfree(args); + + return err; +} + /* * INVOKE_DYNAMIC: the argument descriptors are supplied by user space, which * is also responsible for having imported every buffer to a GEM handle. @@ -412,6 +617,14 @@ int qda_fastrpc_prepare_args(struct qda_fastrpc_invoke_ctx *ctx, void *argp) int err; switch (ctx->type) { + case FASTRPC_RMID_INIT_RELEASE: + err = qda_fastrpc_prepare_args_release_process(ctx); + break; + case FASTRPC_RMID_INIT_CREATE: + case FASTRPC_RMID_INIT_CREATE_ATTR: + ctx->pd = QDA_USER_PD; + err = qda_fastrpc_prepare_args_init_create(ctx, argp); + break; case FASTRPC_RMID_INVOKE_DYNAMIC: err = qda_fastrpc_prepare_args_invoke(ctx, argp); break; diff --git a/drivers/accel/qda/qda_fastrpc.h b/drivers/accel/qda/qda_fastrpc.h index a25818923a6e..6beaa9538983 100644 --- a/drivers/accel/qda/qda_fastrpc.h +++ b/drivers/accel/qda/qda_fastrpc.h @@ -107,6 +107,27 @@ struct fastrpc_invoke_buf { u32 pgidx; }; +/** + * struct fastrpc_create_process_inbuf - Input buffer for process creation + * + * This structure defines the input buffer format for creating a new + * process on the remote DSP. + */ +struct fastrpc_create_process_inbuf { + /** @remote_session_id: Client identifier for the session */ + int remote_session_id; + /** @namelen: Length of the process name string including NUL terminator */ + u32 namelen; + /** @filelen: Length of the ELF shell file in bytes */ + u32 filelen; + /** @pageslen: Number of physical page descriptors */ + u32 pageslen; + /** @attrs: Process attribute flags */ + u32 attrs; + /** @siglen: Length of the signature data in bytes */ + u32 siglen; +}; + /** * struct fastrpc_msg - FastRPC wire message for remote invocations * @@ -192,6 +213,8 @@ struct qda_fastrpc_invoke_ctx { struct qda_gem_obj *msg_gem_obj; /** @file_priv: DRM file private data */ struct drm_file *file_priv; + /** @init_mem_gem_obj: GEM object for PD initialization memory */ + struct qda_gem_obj *init_mem_gem_obj; /** * @req: Request buffer for the internal init/map/unmap calls. Points * into a kernel-owned GEM mapping tracked by @gem_objs, so it must @@ -226,11 +249,23 @@ struct qda_msg { }; /* Remote Method ID table - identifies initialization and control operations */ +#define FASTRPC_RMID_INIT_RELEASE 1 /* Release DSP process */ +#define FASTRPC_RMID_INIT_CREATE 6 /* Create DSP process */ +#define FASTRPC_RMID_INIT_CREATE_ATTR 7 /* Create DSP process with attributes */ #define FASTRPC_RMID_INVOKE_DYNAMIC 0xFFFFFFFFU /* Dynamic method invocation */ /* Common handle for initialization operations */ #define FASTRPC_INIT_HANDLE 0x1 +/* Protection Domain (PD) identifiers */ +#define QDA_ROOT_PD (0) +#define QDA_USER_PD (1) + +/* Number of arguments for process creation */ +#define FASTRPC_CREATE_PROCESS_NARGS 6 +/* Maximum initialization file size (4 MB) */ +#define FASTRPC_INIT_FILELEN_MAX (4 * 1024 * 1024) + void qda_fastrpc_context_free(struct kref *ref); void qda_fastrpc_cleanup_handlelist(struct qda_fastrpc_invoke_ctx *ctx); void qda_fastrpc_flush_pending(struct qda_dev *qdev); diff --git a/drivers/accel/qda/qda_ioctl.c b/drivers/accel/qda/qda_ioctl.c index b7ee4899ba74..34b71b82a020 100644 --- a/drivers/accel/qda/qda_ioctl.c +++ b/drivers/accel/qda/qda_ioctl.c @@ -106,6 +106,40 @@ static int qda_fastrpc_invoke(u32 type, struct drm_device *dev, void *data, ctx->file_priv = file_priv; ctx->remote_session_id = qda_file_priv->remote_session_id; + if (type == FASTRPC_RMID_INIT_CREATE) { + /* + * A second INIT_CREATE on the same file without an intervening + * INIT_RELEASE replaces the process image, so drop the + * initialisation memory cached for the previous attempt. + */ + if (qda_file_priv->init_mem_gem_obj) { + drm_gem_object_put(&qda_file_priv->init_mem_gem_obj->base); + qda_file_priv->init_mem_gem_obj = NULL; + } + + gem_obj = qda_gem_create_object(dev, qdev->iommu_mgr, + FASTRPC_INIT_FILELEN_MAX, file_priv); + if (IS_ERR(gem_obj)) { + err = PTR_ERR(gem_obj); + goto err_context_free; + } + + ctx->init_mem_gem_obj = to_qda_gem_obj(gem_obj); + + /* + * The file private data caches the initialisation memory for + * the lifetime of the DSP process, so it holds a reference of + * its own; the context drops the one taken here when it is + * released. + */ + drm_gem_object_get(gem_obj); + qda_file_priv->init_mem_gem_obj = ctx->init_mem_gem_obj; + } else if (type == FASTRPC_RMID_INIT_RELEASE) { + ctx->init_mem_gem_obj = qda_file_priv->init_mem_gem_obj; + if (ctx->init_mem_gem_obj) + drm_gem_object_get(&ctx->init_mem_gem_obj->base); + } + err = qda_fastrpc_prepare_args(ctx, data); if (err) goto err_context_free; @@ -144,16 +178,59 @@ static int qda_fastrpc_invoke(u32 type, struct drm_device *dev, void *data, qda_fastrpc_cleanup_handlelist(ctx); + /* + * The DSP has torn down the protection domain, so the initialisation + * memory can be released now. + */ + if (type == FASTRPC_RMID_INIT_RELEASE && qda_file_priv->init_mem_gem_obj) { + drm_gem_object_put(&qda_file_priv->init_mem_gem_obj->base); + qda_file_priv->init_mem_gem_obj = NULL; + } + kref_put(&ctx->refcount, qda_fastrpc_context_free); return 0; err_context_free: + /* + * Process creation failed, so release the initialisation memory cached + * in the file private data; the context drops its own reference below. + */ + if (type == FASTRPC_RMID_INIT_CREATE && qda_file_priv->init_mem_gem_obj) { + drm_gem_object_put(&qda_file_priv->init_mem_gem_obj->base); + qda_file_priv->init_mem_gem_obj = NULL; + } + kref_put(&ctx->refcount, qda_fastrpc_context_free); return err; } +/** + * qda_ioctl_init_create() - Create a DSP process + * @dev: DRM device structure + * @data: User-space data (struct drm_qda_init_create) + * @file_priv: DRM file private data + * + * Return: 0 on success, negative error code on failure + */ +int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *file_priv) +{ + return qda_fastrpc_invoke(FASTRPC_RMID_INIT_CREATE, dev, data, file_priv); +} + +/** + * qda_release_dsp_process() - Release DSP process resources for a file + * @qdev: QDA device structure + * @file_priv: DRM file private data + * + * Return: 0 on success, negative error code on failure + */ +int qda_release_dsp_process(struct qda_dev *qdev, struct drm_file *file_priv) +{ + return qda_fastrpc_invoke(FASTRPC_RMID_INIT_RELEASE, &qdev->drm_dev, NULL, file_priv); +} + /** * qda_ioctl_invoke() - Perform a dynamic FastRPC method invocation * @dev: DRM device structure diff --git a/drivers/accel/qda/qda_ioctl.h b/drivers/accel/qda/qda_ioctl.h index 3bb9cfd98370..192565434363 100644 --- a/drivers/accel/qda/qda_ioctl.h +++ b/drivers/accel/qda/qda_ioctl.h @@ -9,6 +9,7 @@ #include "qda_drv.h" int qda_ioctl_query(struct drm_device *dev, void *data, struct drm_file *file_priv); +int qda_ioctl_init_create(struct drm_device *dev, void *data, struct drm_file *file_priv); int qda_ioctl_gem_create(struct drm_device *dev, void *data, struct drm_file *file_priv); int qda_ioctl_gem_mmap_offset(struct drm_device *dev, void *data, struct drm_file *file_priv); int qda_ioctl_invoke(struct drm_device *dev, void *data, struct drm_file *file_priv); diff --git a/include/uapi/drm/qda_accel.h b/include/uapi/drm/qda_accel.h index 5cf0fec1d91c..d12b8dc44fdd 100644 --- a/include/uapi/drm/qda_accel.h +++ b/include/uapi/drm/qda_accel.h @@ -21,7 +21,8 @@ extern "C" { #define DRM_QDA_QUERY 0x00 #define DRM_QDA_GEM_CREATE 0x01 #define DRM_QDA_GEM_MMAP_OFFSET 0x02 -/* Command numbers 0x03-0x06 reserved for INIT_ATTACH, INIT_CREATE, MAP, MUNMAP */ +/* Command number 0x03 reserved for INIT_ATTACH; 0x05-0x06 reserved for MAP, MUNMAP */ +#define DRM_QDA_REMOTE_SESSION_CREATE 0x04 #define DRM_QDA_REMOTE_INVOKE 0x07 /* @@ -37,6 +38,9 @@ extern "C" { struct drm_qda_gem_create) #define DRM_IOCTL_QDA_GEM_MMAP_OFFSET DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_GEM_MMAP_OFFSET, \ struct drm_qda_gem_mmap_offset) +#define DRM_IOCTL_QDA_REMOTE_SESSION_CREATE \ + DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_SESSION_CREATE, \ + struct drm_qda_init_create) #define DRM_IOCTL_QDA_REMOTE_INVOKE DRM_IOWR(DRM_COMMAND_BASE + DRM_QDA_REMOTE_INVOKE, \ struct drm_qda_invoke_args) @@ -126,6 +130,31 @@ struct drm_qda_invoke_args { __u64 args; }; +/** + * struct drm_qda_init_create - Accelerator process initialization parameters + * @filelen: Length of the ELF file in bytes + * @filehandle: GEM handle for the buffer containing the ELF file; 0 if using + * the direct file pointer instead + * @attrs: Process attributes flags + * @siglen: Length of signature data in bytes + * @file: Pointer to ELF file data if not using filehandle + * + * This structure is used with DRM_IOCTL_QDA_INIT_CREATE to initialize + * a new process on the accelerator. The process code is provided either + * via a GEM handle (filehandle) or a direct pointer (file). + * Set file to 0 if using filehandle. + * + * The attrs field contains bit flags for debug mode, privileged execution, + * and other process attributes. + */ +struct drm_qda_init_create { + __u32 filelen; + __u32 filehandle; + __u32 attrs; + __u32 siglen; + __u64 file; +}; + #if defined(__cplusplus) } #endif -- 2.34.1
