--- /dev/null
+/*
+ GUSE: GPIO character device in Userspace
+ Copyright (C) 2025 Nikita Shubin <n.shubin@yadro.com>
+
+ This program can be distributed under the terms of the GNU LGPLv2.
+ See the file COPYING.LIB.
+
+ Read example/cusexmp.c for usages.
+*/
+
+#ifndef GUSE_LOWLEVEL_H_
+#define GUSE_LOWLEVEL_H_
+
+#ifndef FUSE_USE_VERSION
+#define FUSE_USE_VERSION 29
+#endif
+
+#include "fuse_lowlevel.h"
+
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+
+#define GUSE_DEVICE_INODE_FLAG BIT_ULL(63)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct fuse_session;
+
+struct guse_info {
+ unsigned dev_major;
+ unsigned dev_minor;
+ unsigned dev_info_argc;
+ const char **dev_info_argv;
+ unsigned flags;
+};
+
+/*
+ * Most ops behave almost identically to the matching fuse_lowlevel
+ * ops except that they don't take @ino.
+ *
+ * init_done : called after initialization is complete
+ * read : always direct IO, simultaneous operations allowed
+ * ioctl : might be in unrestricted mode depending on ci->flags
+ */
+struct guse_cdev_lowlevel_ops {
+ void (*init) (void *userdata, struct fuse_conn_info *conn);
+ void (*init_done) (void *userdata);
+ void (*destroy) (void *userdata);
+ void (*open) (fuse_req_t req, fuse_ino_t ino,
+ struct fuse_file_info *fi);
+ void (*read) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
+ struct fuse_file_info *fi);
+ void (*release) (fuse_req_t req, fuse_ino_t ino,
+ struct fuse_file_info *fi);
+ void (*ioctl) (fuse_req_t req, fuse_ino_t ino, unsigned int cmd,
+ void *arg, struct fuse_file_info *fi, unsigned flags,
+ const void *in_buf, size_t in_bufsz, size_t out_bufsz);
+ void (*poll) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi,
+ struct fuse_pollhandle *ph);
+};
+
+struct fuse_session *guse_lowlevel_new(struct fuse_args *args,
+ const struct guse_info *ci,
+ const struct guse_cdev_lowlevel_ops *clop,
+ void *userdata);
+
+struct fuse_session *guse_lowlevel_setup(int argc, char *argv[],
+ const struct guse_info *ci,
+ const struct guse_cdev_lowlevel_ops *clop,
+ int *multithreaded, void *userdata);
+
+void guse_lowlevel_teardown(struct fuse_session *se);
+
+int guse_lowlevel_main(int argc, char *argv[], const struct guse_info *ci,
+ const struct guse_cdev_lowlevel_ops *clop, void *userdata);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GUSE_LOWLEVEL_H_ */
--- /dev/null
+/*
+ GUSE: Character device in Userspace
+ Copyright (C) 2025 Nikita Shubin <n.shubin@yadro.com>
+
+ This program can be distributed under the terms of the GNU LGPLv2.
+ See the file COPYING.LIB.
+*/
+
+#include "fuse_config.h"
+#include "guse_lowlevel.h"
+#include "fuse_kernel.h"
+#include "fuse_i.h"
+#include "fuse_opt.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <errno.h>
+#include <unistd.h>
+
+struct guse_data {
+ struct guse_cdev_lowlevel_ops glop;
+ unsigned max_read;
+ unsigned dev_major;
+ unsigned dev_minor;
+ unsigned flags;
+ unsigned dev_info_len;
+ char dev_info[];
+};
+
+static struct guse_cdev_lowlevel_ops *req_glop(fuse_req_t req)
+{
+ return &req->se->guse_data->glop;
+}
+
+static size_t guse_pack_info(int argc, const char **argv, char *buf)
+{
+ size_t size = 0;
+ int i;
+
+ for (i = 0; i < argc; i++) {
+ size_t len;
+
+ len = strlen(argv[i]) + 1;
+ size += len;
+ if (buf) {
+ memcpy(buf, argv[i], len);
+ buf += len;
+ }
+ }
+
+ return size;
+}
+
+static struct guse_data *guse_prep_data(const struct guse_info *ci,
+ const struct guse_cdev_lowlevel_ops *glop)
+{
+ struct guse_data *cd;
+ size_t dev_info_len;
+
+ dev_info_len = guse_pack_info(ci->dev_info_argc, ci->dev_info_argv,
+ NULL);
+
+ if (dev_info_len > CUSE_INIT_INFO_MAX) {
+ fuse_log(FUSE_LOG_ERR, "guse: dev_info (%zu) too large, limit=%u\n",
+ dev_info_len, CUSE_INIT_INFO_MAX);
+ return NULL;
+ }
+
+ cd = calloc(1, sizeof(*cd) + dev_info_len);
+ if (!cd) {
+ fuse_log(FUSE_LOG_ERR, "guse: failed to allocate guse_data\n");
+ return NULL;
+ }
+
+ memcpy(&cd->glop, glop, sizeof(cd->glop));
+ cd->max_read = 131072;
+ cd->dev_major = ci->dev_major;
+ cd->dev_minor = ci->dev_minor;
+ cd->dev_info_len = dev_info_len;
+ cd->flags = ci->flags;
+ guse_pack_info(ci->dev_info_argc, ci->dev_info_argv, cd->dev_info);
+
+ return cd;
+}
+
+struct fuse_session *guse_lowlevel_new(struct fuse_args *args,
+ const struct guse_info *ci,
+ const struct guse_cdev_lowlevel_ops *glop,
+ void *userdata)
+{
+ struct fuse_lowlevel_ops lop;
+ struct guse_data *cd;
+ struct fuse_session *se;
+
+ cd = guse_prep_data(ci, glop);
+ if (!cd)
+ return NULL;
+
+ memset(&lop, 0, sizeof(lop));
+ lop.init = glop->init;
+ lop.destroy = glop->destroy;
+
+ /* TODO: sanity check */
+
+ /* TODO: they are all required for normal operations */
+ lop.open = glop->open;
+ lop.read = glop->read;
+ lop.release = glop->release;
+ lop.ioctl = glop->ioctl;
+ lop.poll = glop->poll;
+
+ se = fuse_session_new(args, &lop, sizeof(lop), userdata);
+ if (!se)
+ goto out_fail;
+
+ se->guse_data = cd;
+
+ return se;
+
+out_fail:
+ free(cd);
+ return NULL;
+}
+
+static int guse_reply_init(fuse_req_t req, struct cuse_init_out *arg,
+ char *dev_info, unsigned dev_info_len)
+{
+ struct iovec iov[3] = { { 0 } };
+
+ iov[1].iov_base = arg;
+ iov[1].iov_len = sizeof(struct cuse_init_out);
+ iov[2].iov_base = dev_info;
+ iov[2].iov_len = dev_info_len;
+
+ return fuse_send_reply_iov_nofree(req, 0, iov, 3);
+}
+
+void guse_lowlevel_init(fuse_req_t req, fuse_ino_t nodeid, const void *inarg)
+{
+ struct fuse_init_in *arg = (struct fuse_init_in *) inarg;
+ struct cuse_init_out outarg;
+ struct fuse_session *se = req->se;
+ struct guse_data *cd = se->guse_data;
+ size_t bufsize = se->bufsize;
+ struct guse_cdev_lowlevel_ops *glop = req_glop(req);
+
+ (void) nodeid;
+ if (se->debug) {
+ fuse_log(FUSE_LOG_DEBUG, "GUSE_INIT: %u.%u\n", arg->major, arg->minor);
+ fuse_log(FUSE_LOG_DEBUG, "flags=0x%08x\n", arg->flags);
+ }
+ se->conn.proto_major = arg->major;
+ se->conn.proto_minor = arg->minor;
+ se->conn.capable = 0;
+ se->conn.want = 0;
+
+ if (arg->major < 7) {
+ fuse_log(FUSE_LOG_ERR, "guse: unsupported protocol version: %u.%u\n",
+ arg->major, arg->minor);
+ fuse_reply_err(req, EPROTO);
+ return;
+ }
+
+ if (bufsize < FUSE_MIN_READ_BUFFER) {
+ fuse_log(FUSE_LOG_ERR, "guse: warning: buffer size too small: %zu\n",
+ bufsize);
+ bufsize = FUSE_MIN_READ_BUFFER;
+ }
+
+ bufsize -= 4096;
+ if (bufsize < se->conn.max_write)
+ se->conn.max_write = bufsize;
+
+ se->got_init = 1;
+ if (se->op.init)
+ se->op.init(se->userdata, &se->conn);
+
+ memset(&outarg, 0, sizeof(outarg));
+ outarg.major = FUSE_KERNEL_VERSION;
+ outarg.minor = FUSE_KERNEL_MINOR_VERSION;
+ outarg.flags = cd->flags;
+ outarg.max_read = cd->max_read;
+ outarg.max_write = se->conn.max_write;
+ outarg.dev_major = cd->dev_major;
+ outarg.dev_minor = cd->dev_minor;
+
+ if (se->debug) {
+ fuse_log(FUSE_LOG_DEBUG, " GUSE_INIT: %u.%u\n",
+ outarg.major, outarg.minor);
+ fuse_log(FUSE_LOG_DEBUG, " flags=0x%08x\n", outarg.flags);
+ fuse_log(FUSE_LOG_DEBUG, " max_read=0x%08x\n", outarg.max_read);
+ fuse_log(FUSE_LOG_DEBUG, " max_write=0x%08x\n", outarg.max_write);
+ fuse_log(FUSE_LOG_DEBUG, " dev_major=%u\n", outarg.dev_major);
+ fuse_log(FUSE_LOG_DEBUG, " dev_minor=%u\n", outarg.dev_minor);
+ fuse_log(FUSE_LOG_DEBUG, " dev_info: %.*s\n", cd->dev_info_len,
+ cd->dev_info);
+ }
+
+ guse_reply_init(req, &outarg, cd->dev_info, cd->dev_info_len);
+
+ if (glop->init_done)
+ glop->init_done(se->userdata);
+
+ fuse_free_req(req);
+}
+
+struct fuse_session *guse_lowlevel_setup(int argc, char *argv[],
+ const struct guse_info *ci,
+ const struct guse_cdev_lowlevel_ops *glop,
+ int *multithreaded, void *userdata)
+{
+ const char *devname = "/dev/guse";
+ static const struct fuse_opt kill_subtype_opts[] = {
+ FUSE_OPT_KEY("subtype=", FUSE_OPT_KEY_DISCARD),
+ FUSE_OPT_END
+ };
+ struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+ struct fuse_session *se;
+ struct fuse_cmdline_opts opts;
+ int fd;
+ int res;
+
+ if (fuse_parse_cmdline(&args, &opts) == -1)
+ return NULL;
+ *multithreaded = !opts.singlethread;
+
+ /* Remove subtype= option */
+ res = fuse_opt_parse(&args, NULL, kill_subtype_opts, NULL);
+ if (res == -1)
+ goto out1;
+
+ /*
+ * Make sure file descriptors 0, 1 and 2 are open, otherwise chaos
+ * would ensue.
+ */
+ do {
+ fd = open("/dev/null", O_RDWR);
+ if (fd > 2)
+ close(fd);
+ } while (fd >= 0 && fd <= 2);
+
+ se = guse_lowlevel_new(&args, ci, glop, userdata);
+ if (se == NULL)
+ goto out1;
+
+ fd = open(devname, O_RDWR);
+ if (fd == -1) {
+ if (errno == ENODEV || errno == ENOENT)
+ fuse_log(FUSE_LOG_ERR, "guse: device not found, try 'modprobe guse' first\n");
+ else
+ fuse_log(FUSE_LOG_ERR, "guse: failed to open %s: %s\n",
+ devname, strerror(errno));
+ goto err_se;
+ }
+ se->fd = fd;
+
+ res = fuse_set_signal_handlers(se);
+ if (res == -1)
+ goto err_se;
+
+ res = fuse_daemonize(opts.foreground);
+ if (res == -1)
+ goto err_sig;
+
+ fuse_opt_free_args(&args);
+ return se;
+
+err_sig:
+ fuse_remove_signal_handlers(se);
+err_se:
+ fuse_session_destroy(se);
+out1:
+ free(opts.mountpoint);
+ fuse_opt_free_args(&args);
+ return NULL;
+}
+
+void guse_lowlevel_teardown(struct fuse_session *se)
+{
+ fuse_remove_signal_handlers(se);
+ fuse_session_destroy(se);
+}
+
+int guse_lowlevel_main(int argc, char *argv[], const struct guse_info *ci,
+ const struct guse_cdev_lowlevel_ops *glop, void *userdata)
+{
+ struct fuse_session *se;
+ int multithreaded;
+ int res;
+
+ se = guse_lowlevel_setup(argc, argv, ci, glop, &multithreaded,
+ userdata);
+ if (se == NULL)
+ return 1;
+
+ if (multithreaded) {
+ struct fuse_loop_config *config = fuse_loop_cfg_create();
+ res = fuse_session_loop_mt(se, config);
+ fuse_loop_cfg_destroy(config);
+ }
+ else
+ res = fuse_session_loop(se);
+
+ guse_lowlevel_teardown(se);
+ if (res == -1)
+ return 1;
+
+ return 0;
+}