/* * Copyright (c) 2020 Friedt Professional Engineering Services, Inc * * SPDX-License-Identifier: Apache-2.0 */ #include /* Zephyr headers */ #include LOG_MODULE_REGISTER(net_spair, CONFIG_NET_SOCKETS_LOG_LEVEL); #include #include #include #include #include #include "sockets_internal.h" enum { SPAIR_SIG_CANCEL, /**< operation has been canceled */ SPAIR_SIG_DATA, /**< @ref spair.recv_q has been updated */ }; enum { SPAIR_FLAG_NONBLOCK = (1 << 0), /**< socket is non-blocking */ }; #define SPAIR_FLAGS_DEFAULT 0 /** * Socketpair endpoint structure * * This structure represents one half of a socketpair (an 'endpoint'). * * The implementation strives for compatibility with socketpair(2). * * Resources contained within this structure are said to be 'local', while * reources contained within the other half of the socketpair (or other * endpoint) are said to be 'remote'. * * Theory of operation: * - each end of a socketpair owns a @a recv_q * - since there is no write queue, data is either written or not * - read and write operations may return partial transfers * - read operations may block if the local @a recv_q is empty * - write operations may block if the remote @a recv_q is full * - each endpoint may be blocking or non-blocking */ __net_socket struct spair { int remote; /**< the remote endpoint file descriptor */ uint32_t flags; /**< status and option bits */ struct k_sem sem; /**< semaphore for exclusive structure access */ struct k_pipe recv_q; /**< receive queue of local endpoint */ /** indicates write of local @a recv_q occurred */ struct k_poll_signal write_signal; /** indicates read of local @a recv_q occurred */ struct k_poll_signal read_signal; /** buffer for @a recv_q recv_q */ uint8_t buf[CONFIG_NET_SOCKETPAIR_BUFFER_SIZE]; }; /* forward declaration */ static const struct socket_op_vtable spair_fd_op_vtable; #undef sock_is_nonblock /** Determine if a @ref spair is in non-blocking mode */ static inline bool sock_is_nonblock(const struct spair *spair) { return !!(spair->flags & SPAIR_FLAG_NONBLOCK); } /** Determine if a @ref spair is connected */ static inline bool sock_is_connected(const struct spair *spair) { const struct spair *remote = z_get_fd_obj(spair->remote, (const struct fd_op_vtable *)&spair_fd_op_vtable, 0); if (remote == NULL) { return false; } return true; } #undef sock_is_eof /** Determine if a @ref spair has encountered end-of-file */ static inline bool sock_is_eof(const struct spair *spair) { return !sock_is_connected(spair); } /** * Determine bytes available to write * * Specifically, this function calculates the number of bytes that may be * written to a given @ref spair without blocking. */ static inline size_t spair_write_avail(struct spair *spair) { struct spair *const remote = z_get_fd_obj(spair->remote, (const struct fd_op_vtable *)&spair_fd_op_vtable, 0); if (remote == NULL) { return 0; } return k_pipe_write_avail(&remote->recv_q); } /** * Determine bytes available to read * * Specifically, this function calculates the number of bytes that may be * read from a given @ref spair without blocking. */ static inline size_t spair_read_avail(struct spair *spair) { return k_pipe_read_avail(&spair->recv_q); } /** Swap two 32-bit integers */ static inline void swap32(uint32_t *a, uint32_t *b) { uint32_t c; c = *b; *b = *a; *a = c; } /** * Delete @param spair * * This function deletes one endpoint of a socketpair. * * Theory of operation: * - we have a socketpair with two endpoints: A and B * - we have two threads: T1 and T2 * - T1 operates on endpoint A * - T2 operates on endpoint B * * There are two possible cases where a blocking operation must be notified * when one endpoint is closed: * -# T1 is blocked reading from A and T2 closes B * T1 waits on A's write signal. T2 triggers the remote * @ref spair.write_signal * -# T1 is blocked writing to A and T2 closes B * T1 is waits on B's read signal. T2 triggers the local * @ref spair.read_signal. * * If the remote endpoint is already closed, the former operation does not * take place. Otherwise, the @ref spair.remote of the local endpoint is * set to -1. * * If no threads are blocking on A, then the signals have no effect. * * The memeory associated with the local endpoint is cleared and freed. */ static void spair_delete(struct spair *spair) { int res; struct spair *remote = NULL; bool have_remote_sem = false; if (spair == NULL) { return; } if (spair->remote != -1) { remote = z_get_fd_obj(spair->remote, (const struct fd_op_vtable *)&spair_fd_op_vtable, 0); if (remote != NULL) { res = k_sem_take(&remote->sem, K_FOREVER); if (res == 0) { have_remote_sem = true; remote->remote = -1; res = k_poll_signal_raise(&remote->write_signal, SPAIR_SIG_CANCEL); __ASSERT(res == 0, "k_poll_signal_raise() failed: %d", res); } } } spair->remote = -1; res = k_poll_signal_raise(&spair->read_signal, SPAIR_SIG_CANCEL); __ASSERT(res == 0, "k_poll_signal_raise() failed: %d", res); /* ensure no private information is released to the memory pool */ memset(spair, 0, sizeof(*spair)); #ifdef CONFIG_USERSPACE k_object_free(spair); #else k_free(spair); #endif if (remote != NULL && have_remote_sem) { k_sem_give(&remote->sem); } } /** * Create a @ref spair (1/2 of a socketpair) * * The idea is to call this twice, but store the "local" side in the * @ref spair.remote field initially. * * If both allocations are successful, then swap the @ref spair.remote * fields in the two @ref spair instances. */ static struct spair *spair_new(void) { struct spair *spair; #ifdef CONFIG_USERSPACE struct z_object *zo = z_dynamic_object_create(sizeof(*spair)); if (zo == NULL) { spair = NULL; } else { spair = zo->name; zo->type = K_OBJ_NET_SOCKET; } #else spair = k_malloc(sizeof(*spair)); #endif if (spair == NULL) { errno = ENOMEM; goto out; } memset(spair, 0, sizeof(*spair)); /* initialize any non-zero default values */ spair->remote = -1; spair->flags = SPAIR_FLAGS_DEFAULT; k_sem_init(&spair->sem, 1, 1); k_pipe_init(&spair->recv_q, spair->buf, sizeof(spair->buf)); k_poll_signal_init(&spair->write_signal); k_poll_signal_init(&spair->read_signal); spair->remote = z_reserve_fd(); if (spair->remote == -1) { errno = ENFILE; goto cleanup; } z_finalize_fd(spair->remote, spair, (const struct fd_op_vtable *)&spair_fd_op_vtable); goto out; cleanup: spair_delete(spair); spair = NULL; out: return spair; } int z_impl_zsock_socketpair(int family, int type, int proto, int *sv) { int res; size_t i; struct spair *obj[2] = {}; if (family != AF_UNIX) { errno = EAFNOSUPPORT; res = -1; goto errout; } if (type != SOCK_STREAM) { errno = EPROTOTYPE; res = -1; goto errout; } if (proto != 0) { errno = EPROTONOSUPPORT; res = -1; goto errout; } if (sv == NULL) { /* not listed in normative spec, but mimics Linux behaviour */ errno = EFAULT; res = -1; goto errout; } for (i = 0; i < 2; ++i) { obj[i] = spair_new(); if (!obj[i]) { res = -1; goto cleanup; } } /* connect the two endpoints */ swap32(&obj[0]->remote, &obj[1]->remote); for (i = 0; i < 2; ++i) { sv[i] = obj[i]->remote; k_sem_give(&obj[0]->sem); } return 0; cleanup: for (i = 0; i < 2; ++i) { spair_delete(obj[i]); } errout: return res; } #ifdef CONFIG_USERSPACE int z_vrfy_zsock_socketpair(int family, int type, int proto, int *sv) { int ret; int tmp[2]; if (Z_SYSCALL_MEMORY_WRITE(sv, sizeof(tmp)) != 0) { /* not listed in normative spec, but mimics linux behaviour */ errno = EFAULT; ret = -1; goto out; } ret = z_impl_zsock_socketpair(family, type, proto, tmp); if (ret == 0) { Z_OOPS(z_user_to_copy(sv, tmp, sizeof(tmp))); } out: return ret; } #include #endif /* CONFIG_USERSPACE */ /** * Write data to one end of a @ref spair * * Data written on one file descriptor of a socketpair can be read at the * other end using common POSIX calls such as read(2) or recv(2). * * If the underlying file descriptor has the @ref O_NONBLOCK flag set then * this function will return immediately. If no data was written on a * non-blocking file descriptor, then -1 will be returned and @ref errno will * be set to @ref EAGAIN. * * Blocking write operations occur when the @ref O_NONBLOCK flag is @em not * set and there is insufficient space in the @em remote @ref spair.pipe. * * Such a blocking write will suspend execution of the current thread until * one of two possible results is received on the @em remote * @ref spair.read_signal: * * 1) @ref SPAIR_SIG_DATA - data has been read from the @em remote * @ref spair.pipe. Thus, allowing more data to be written. * * 2) @ref SPAIR_SIG_CANCEL - the @em remote socketpair endpoint was closed * Receipt of this result is analagous to SIGPIPE from POSIX * ("Write on a pipe with no one to read it."). In this case, the function * will return -1 and set @ref errno to @ref EPIPE. * * @param obj the address of an @ref spair object cast to `void *` * @param buffer the buffer to write * @param count the number of bytes to write from @p buffer * * @return on success, a number > 0 representing the number of bytes written * @return -1 on error, with @ref errno set appropriately. */ static ssize_t spair_write(void *obj, const void *buffer, size_t count) { int res; int key; size_t avail; bool is_nonblock; size_t bytes_written; bool have_local_sem = false; bool have_remote_sem = false; bool will_block = false; struct spair *const spair = (struct spair *)obj; struct spair *remote = NULL; if (obj == NULL || buffer == NULL || count == 0) { errno = EINVAL; res = -1; goto out; } key = irq_lock(); is_nonblock = sock_is_nonblock(spair); res = k_sem_take(&spair->sem, K_NO_WAIT); irq_unlock(key); if (res < 0) { if (is_nonblock) { errno = EAGAIN; res = -1; goto out; } res = k_sem_take(&spair->sem, K_FOREVER); if (res < 0) { errno = -res; res = -1; goto out; } is_nonblock = sock_is_nonblock(spair); } have_local_sem = true; remote = z_get_fd_obj(spair->remote, (const struct fd_op_vtable *)&spair_fd_op_vtable, 0); if (remote == NULL) { errno = EPIPE; res = -1; goto out; } res = k_sem_take(&remote->sem, K_NO_WAIT); if (res < 0) { if (is_nonblock) { errno = EAGAIN; res = -1; goto out; } res = k_sem_take(&remote->sem, K_FOREVER); if (res < 0) { errno = -res; res = -1; goto out; } } have_remote_sem = true; avail = spair_write_avail(spair); if (avail == 0) { if (is_nonblock) { errno = EAGAIN; res = -1; goto out; } will_block = true; } if (will_block) { for (int signaled = false, result = -1; !signaled; result = -1) { struct k_poll_event events[] = { K_POLL_EVENT_INITIALIZER( K_POLL_TYPE_SIGNAL, K_POLL_MODE_NOTIFY_ONLY, &remote->read_signal), }; k_sem_give(&remote->sem); have_remote_sem = false; res = k_poll(events, ARRAY_SIZE(events), K_FOREVER); if (res < 0) { errno = -res; res = -1; goto out; } remote = z_get_fd_obj(spair->remote, (const struct fd_op_vtable *) &spair_fd_op_vtable, 0); if (remote == NULL) { errno = EPIPE; res = -1; goto out; } res = k_sem_take(&remote->sem, K_FOREVER); if (res < 0) { errno = -res; res = -1; goto out; } have_remote_sem = true; k_poll_signal_check(&remote->read_signal, &signaled, &result); if (!signaled) { continue; } switch (result) { case SPAIR_SIG_DATA: { break; } case SPAIR_SIG_CANCEL: { errno = EPIPE; res = -1; goto out; } default: { __ASSERT(false, "unrecognized result: %d", result); continue; } } /* SPAIR_SIG_DATA was received */ break; } } res = k_pipe_put(&remote->recv_q, (void *)buffer, count, &bytes_written, 1, K_NO_WAIT); __ASSERT(res == 0, "k_pipe_put() failed: %d", res); res = k_poll_signal_raise(&remote->write_signal, SPAIR_SIG_DATA); __ASSERT(res == 0, "k_poll_signal_raise() failed: %d", res); res = bytes_written; out: if (remote != NULL && have_remote_sem) { k_sem_give(&remote->sem); } if (spair != NULL && have_local_sem) { k_sem_give(&spair->sem); } return res; } /** * Read data from one end of a @ref spair * * Data written on one file descriptor of a socketpair (with e.g. write(2) or * send(2)) can be read at the other end using common POSIX calls such as * read(2) or recv(2). * * If the underlying file descriptor has the @ref O_NONBLOCK flag set then * this function will return immediately. If no data was read from a * non-blocking file descriptor, then -1 will be returned and @ref errno will * be set to @ref EAGAIN. * * Blocking read operations occur when the @ref O_NONBLOCK flag is @em not set * and there are no bytes to read in the @em local @ref spair.pipe. * * Such a blocking read will suspend execution of the current thread until * one of two possible results is received on the @em local * @ref spair.write_signal: * * -# @ref SPAIR_SIG_DATA - data has been written to the @em local * @ref spair.pipe. Thus, allowing more data to be read. * * -# @ref SPAIR_SIG_CANCEL - read of the the @em local @spair.pipe * must be cancelled for some reason (e.g. the file descriptor will be * closed imminently). In this case, the function will return -1 and set * @ref errno to @ref EINTR. * * @param obj the address of an @ref spair object cast to `void *` * @param buffer the buffer in which to read * @param count the number of bytes to read * * @return on success, a number > 0 representing the number of bytes written * @return -1 on error, with @ref errno set appropriately. */ static ssize_t spair_read(void *obj, void *buffer, size_t count) { int res; int key; bool is_connected; size_t avail; bool is_nonblock; size_t bytes_read; bool have_local_sem = false; bool will_block = false; struct spair *const spair = (struct spair *)obj; if (obj == NULL || buffer == NULL || count == 0) { errno = EINVAL; res = -1; goto out; } key = irq_lock(); is_nonblock = sock_is_nonblock(spair); res = k_sem_take(&spair->sem, K_NO_WAIT); irq_unlock(key); if (res < 0) { if (is_nonblock) { errno = EAGAIN; res = -1; goto out; } res = k_sem_take(&spair->sem, K_FOREVER); if (res < 0) { errno = -res; res = -1; goto out; } is_nonblock = sock_is_nonblock(spair); } have_local_sem = true; is_connected = sock_is_connected(spair); avail = spair_read_avail(spair); if (avail == 0) { if (!is_connected) { /* signal EOF */ res = 0; goto out; } if (is_nonblock) { errno = EAGAIN; res = -1; goto out; } will_block = true; } if (will_block) { for (int signaled = false, result = -1; !signaled; result = -1) { struct k_poll_event events[] = { K_POLL_EVENT_INITIALIZER( K_POLL_TYPE_SIGNAL, K_POLL_MODE_NOTIFY_ONLY, &spair->write_signal ), }; k_sem_give(&spair->sem); have_local_sem = false; res = k_poll(events, ARRAY_SIZE(events), K_FOREVER); __ASSERT(res == 0, "k_poll() failed: %d", res); res = k_sem_take(&spair->sem, K_FOREVER); __ASSERT(res == 0, "failed to take local sem: %d", res); have_local_sem = true; k_poll_signal_check(&spair->write_signal, &signaled, &result); if (!signaled) { continue; } switch (result) { case SPAIR_SIG_DATA: { break; } case SPAIR_SIG_CANCEL: { errno = EPIPE; res = -1; goto out; } default: { __ASSERT(false, "unrecognized result: %d", result); continue; } } /* SPAIR_SIG_DATA was received */ break; } } res = k_pipe_get(&spair->recv_q, (void *)buffer, count, &bytes_read, 1, K_NO_WAIT); __ASSERT(res == 0, "k_pipe_get() failed: %d", res); if (is_connected) { res = k_poll_signal_raise(&spair->read_signal, SPAIR_SIG_DATA); __ASSERT(res == 0, "k_poll_signal_raise() failed: %d", res); } res = bytes_read; out: if (spair != NULL && have_local_sem) { k_sem_give(&spair->sem); } return res; } static int zsock_poll_prepare_ctx(struct spair *const spair, struct zsock_pollfd *const pfd, struct k_poll_event **pev, struct k_poll_event *pev_end) { int res; struct spair *remote = NULL; bool have_remote_sem = false; if (pfd->events & ZSOCK_POLLIN) { /* Tell poll() to short-circuit wait */ if (sock_is_eof(spair)) { res = -EALREADY; goto out; } if (*pev == pev_end) { res = -ENOMEM; goto out; } /* Wait until data has been written to the local end */ (*pev)->obj = &spair->write_signal; } if (pfd->events & ZSOCK_POLLOUT) { /* Tell poll() to short-circuit wait */ if (!sock_is_connected(spair)) { res = -EALREADY; goto out; } if (*pev == pev_end) { res = -ENOMEM; goto out; } remote = z_get_fd_obj(spair->remote, (const struct fd_op_vtable *) &spair_fd_op_vtable, 0); __ASSERT(remote != NULL, "remote is NULL"); res = k_sem_take(&remote->sem, K_FOREVER); if (res < 0) { goto out; } have_remote_sem = true; /* Wait until data has been read from the remote end */ (*pev)->obj = &remote->read_signal; } (*pev)->type = K_POLL_TYPE_SIGNAL; (*pev)->mode = K_POLL_MODE_NOTIFY_ONLY; (*pev)->state = K_POLL_STATE_NOT_READY; k_poll_signal_reset((*pev)->obj); (*pev)++; res = 0; out: if (remote != NULL && have_remote_sem) { k_sem_give(&remote->sem); } return res; } static int zsock_poll_update_ctx(struct spair *const spair, struct zsock_pollfd *const pfd, struct k_poll_event **pev) { int res; int signaled; int result; struct spair *remote = NULL; bool have_remote_sem = false; if (pfd->events & ZSOCK_POLLOUT) { if (!sock_is_connected(spair)) { pfd->revents |= ZSOCK_POLLHUP; goto pollout_done; } remote = z_get_fd_obj(spair->remote, (const struct fd_op_vtable *) &spair_fd_op_vtable, 0); __ASSERT(remote != NULL, "remote is NULL"); res = k_sem_take(&remote->sem, K_FOREVER); if (res < 0) { /* if other end is deleted, this might occur */ goto pollout_done; } have_remote_sem = true; if (spair_write_avail(spair) > 0) { pfd->revents |= ZSOCK_POLLOUT; goto pollout_done; } /* check to see if op was canceled */ signaled = false; k_poll_signal_check(&remote->read_signal, &signaled, &result); if (signaled) { /* Cannot be SPAIR_SIG_DATA, because * spair_write_avail() would have * returned 0 */ __ASSERT(result == SPAIR_SIG_CANCEL, "invalid result %d", result); pfd->revents |= ZSOCK_POLLHUP; } } pollout_done: if (pfd->events & ZSOCK_POLLIN) { if (sock_is_eof(spair)) { pfd->revents |= ZSOCK_POLLIN; goto pollin_done; } if (spair_read_avail(spair) > 0) { pfd->revents |= ZSOCK_POLLIN; goto pollin_done; } /* check to see if op was canceled */ signaled = false; k_poll_signal_check(&spair->write_signal, &signaled, &result); if (signaled) { /* Cannot be SPAIR_SIG_DATA, because * spair_read_avail() would have * returned 0 */ __ASSERT(result == SPAIR_SIG_CANCEL, "invalid result %d", result); pfd->revents |= ZSOCK_POLLIN; } } pollin_done: res = 0; (*pev)++; if (remote != NULL && have_remote_sem) { k_sem_give(&remote->sem); } return res; } static int spair_ioctl(void *obj, unsigned int request, va_list args) { int res; struct zsock_pollfd *pfd; struct k_poll_event **pev; struct k_poll_event *pev_end; int flags = 0; bool have_local_sem = false; struct spair *const spair = (struct spair *)obj; if (spair == NULL) { errno = EINVAL; res = -1; goto out; } /* The local sem is always taken in this function. If a subsequent * function call requires the remote sem, it must acquire and free the * remote sem. */ res = k_sem_take(&spair->sem, K_FOREVER); __ASSERT(res == 0, "failed to take local sem: %d", res); have_local_sem = true; switch (request) { case F_GETFL: { if (sock_is_nonblock(spair)) { flags |= O_NONBLOCK; } res = flags; goto out; } case F_SETFL: { flags = va_arg(args, int); if (flags & O_NONBLOCK) { spair->flags |= SPAIR_FLAG_NONBLOCK; } else { spair->flags &= ~SPAIR_FLAG_NONBLOCK; } res = 0; goto out; } case ZFD_IOCTL_CLOSE: { /* disconnect the remote endpoint */ spair_delete(spair); have_local_sem = false; res = 0; goto out; } case ZFD_IOCTL_POLL_PREPARE: { pfd = va_arg(args, struct zsock_pollfd *); pev = va_arg(args, struct k_poll_event **); pev_end = va_arg(args, struct k_poll_event *); res = zsock_poll_prepare_ctx(obj, pfd, pev, pev_end); goto out; } case ZFD_IOCTL_POLL_UPDATE: { pfd = va_arg(args, struct zsock_pollfd *); pev = va_arg(args, struct k_poll_event **); res = zsock_poll_update_ctx(obj, pfd, pev); goto out; } default: { errno = EOPNOTSUPP; res = -1; goto out; } } out: if (spair != NULL && have_local_sem) { k_sem_give(&spair->sem); } return res; } static int spair_bind(void *obj, const struct sockaddr *addr, socklen_t addrlen) { ARG_UNUSED(obj); ARG_UNUSED(addr); ARG_UNUSED(addrlen); errno = EISCONN; return -1; } static int spair_connect(void *obj, const struct sockaddr *addr, socklen_t addrlen) { ARG_UNUSED(obj); ARG_UNUSED(addr); ARG_UNUSED(addrlen); errno = EISCONN; return -1; } static int spair_listen(void *obj, int backlog) { ARG_UNUSED(obj); ARG_UNUSED(backlog); errno = EINVAL; return -1; } static int spair_accept(void *obj, struct sockaddr *addr, socklen_t *addrlen) { ARG_UNUSED(obj); ARG_UNUSED(addr); ARG_UNUSED(addrlen); errno = EOPNOTSUPP; return -1; } static ssize_t spair_sendto(void *obj, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) { ARG_UNUSED(flags); ARG_UNUSED(dest_addr); ARG_UNUSED(addrlen); return spair_write(obj, buf, len); } static ssize_t spair_sendmsg(void *obj, const struct msghdr *msg, int flags) { ARG_UNUSED(flags); int res; size_t len = 0; bool is_connected; size_t avail; bool is_nonblock; struct spair *const spair = (struct spair *)obj; if (spair == NULL || msg == NULL) { errno = EINVAL; res = -1; goto out; } is_connected = sock_is_connected(spair); avail = is_connected ? spair_write_avail(spair) : 0; is_nonblock = sock_is_nonblock(spair); for (size_t i = 0; i < msg->msg_iovlen; ++i) { /* check & msg->msg_iov[i]? */ /* check & msg->msg_iov[i].iov_base? */ len += msg->msg_iov[i].iov_len; } if (!is_connected) { errno = EPIPE; res = -1; goto out; } if (len == 0) { res = 0; goto out; } if (len > avail && is_nonblock) { errno = EMSGSIZE; res = -1; goto out; } for (size_t i = 0; i < msg->msg_iovlen; ++i) { res = spair_write(spair, msg->msg_iov[i].iov_base, msg->msg_iov[i].iov_len); if (res == -1) { goto out; } } res = len; out: return res; } static ssize_t spair_recvfrom(void *obj, void *buf, size_t max_len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) { (void)flags; (void)src_addr; (void)addrlen; if (addrlen != NULL) { /* Protocol (PF_UNIX) does not support addressing with connected * sockets and, therefore, it is unspecified behaviour to modify * src_addr. However, it would be ambiguous to leave addrlen * untouched if the user expects it to be updated. It is not * mentioned that modifying addrlen is unspecified. Therefore * we choose to eliminate ambiguity. * * Setting it to zero mimics Linux's behaviour. */ *addrlen = 0; } return spair_read(obj, buf, max_len); } static int spair_getsockopt(void *obj, int level, int optname, void *optval, socklen_t *optlen) { ARG_UNUSED(obj); ARG_UNUSED(level); ARG_UNUSED(optname); ARG_UNUSED(optval); ARG_UNUSED(optlen); errno = ENOPROTOOPT; return -1; } static int spair_setsockopt(void *obj, int level, int optname, const void *optval, socklen_t optlen) { ARG_UNUSED(obj); ARG_UNUSED(level); ARG_UNUSED(optname); ARG_UNUSED(optval); ARG_UNUSED(optlen); errno = ENOPROTOOPT; return -1; } static const struct socket_op_vtable spair_fd_op_vtable = { .fd_vtable = { .read = spair_read, .write = spair_write, .ioctl = spair_ioctl, }, .bind = spair_bind, .connect = spair_connect, .listen = spair_listen, .accept = spair_accept, .sendto = spair_sendto, .sendmsg = spair_sendmsg, .recvfrom = spair_recvfrom, .getsockopt = spair_getsockopt, .setsockopt = spair_setsockopt, };