From 8e8d0436ad8008e5a3d3ed568ee56c9edc2bd306 Mon Sep 17 00:00:00 2001 From: Velikiy Kirill Date: Wed, 13 Aug 2025 03:41:09 +0300 Subject: [PATCH] Add epoll bindings to std::os::linux + misc (#2350) * Add epoll to std::os::linux + misc * Fix import in linux.c3 * epoll: Add unit tests * epoll: Fix imports in unit tests * epoll: Add libc import --- lib/std/os/linux/epoll.c3 | 139 +++++++++++++++++++++++++++++ lib/std/os/linux/linux.c3 | 31 ++++++- test/unit/stdlib/os/linux_epoll.c3 | 123 +++++++++++++++++++++++++ 3 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 lib/std/os/linux/epoll.c3 create mode 100644 test/unit/stdlib/os/linux_epoll.c3 diff --git a/lib/std/os/linux/epoll.c3 b/lib/std/os/linux/epoll.c3 new file mode 100644 index 000000000..2d2346194 --- /dev/null +++ b/lib/std/os/linux/epoll.c3 @@ -0,0 +1,139 @@ +module std::os::linux @if(env::LINUX); +import libc; + +// https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/sys/epoll.h +const uint EPOLLIN = EpollEvents.EPOLLIN; +const uint EPOLLPRI = EpollEvents.EPOLLPRI; +const uint EPOLLOUT = EpollEvents.EPOLLOUT; +const uint EPOLLRDNORM = EpollEvents.EPOLLRDNORM; +const uint EPOLLRDBAND = EpollEvents.EPOLLRDBAND; +const uint EPOLLWRNORM = EpollEvents.EPOLLWRNORM; +const uint EPOLLWRBAND = EpollEvents.EPOLLWRBAND; +const uint EPOLLMSG = EpollEvents.EPOLLMSG; +const uint EPOLLERR = EpollEvents.EPOLLERR; +const uint EPOLLHUP = EpollEvents.EPOLLHUP; +const uint EPOLLRDHUP = EpollEvents.EPOLLRDHUP; +const uint EPOLLEXCLUSIVE = EpollEvents.EPOLLEXCLUSIVE; +const uint EPOLLWAKEUP = EpollEvents.EPOLLWAKEUP; +const uint EPOLLONESHOT = EpollEvents.EPOLLONESHOT; +const uint EPOLLET = EpollEvents.EPOLLET; + +enum EpollEvents: const inline uint +{ + EPOLLIN = 0x001, + EPOLLPRI = 0x002, + EPOLLOUT = 0x004, + EPOLLRDNORM = 0x040, + EPOLLRDBAND = 0x080, + EPOLLWRNORM = 0x100, + EPOLLWRBAND = 0x200, + EPOLLMSG = 0x400, + EPOLLERR = 0x008, + EPOLLHUP = 0x010, + EPOLLRDHUP = 0x2000, + EPOLLEXCLUSIVE = 1u << 28, + EPOLLWAKEUP = 1u << 29, + EPOLLONESHOT = 1u << 30, + EPOLLET = 1u << 31 +} + +/* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */ +const uint EPOLL_CTL_ADD = 1; /* Add a file descriptor to the interface. */ +const uint EPOLL_CTL_DEL = 2; /* Remove a file descriptor from the interface. */ +const uint EPOLL_CTL_MOD = 3; /* Change file descriptor epoll_event structure. */ + +union EpollData +{ + void* ptr; + int fd; + uint u32; + ulong u64; +} + +struct EpollEvent @packed +{ + uint events; /* Epoll events */ + EpollData data; /* User data variable */ +} + +struct EpollParams +{ + uint busy_poll_usecs; + ushort busy_poll_budget; + char prefer_busy_poll; + + /* pad the struct to a multiple of 64bits */ + char __pad; +} + +// https://github.com/MatthiasWM/mosrun/blob/master/scratchpad.txt#L330-L348 +/* + * Ioctl's have the command encoded in the lower word, + * and the size of any in or out parameters in the upper + * word. The high 2 bits of the upper word are used + * to encode the in/out status of the parameter; for now + * we restrict parameters to at most 256 bytes (disklabels are 216 bytes). + */ +const IOCPARM_MASK = 0xff; /* parameters must be < 256 bytes */ +const IOC_VOID = 0x20000000; /* no parameters */ +const IOC_OUT = 0x40000000; /* copy out parameters */ +const IOC_IN = 0x80000000; /* copy in parameters */ +const IOC_INOUT = (IOC_IN|IOC_OUT); + +macro ulong @ioctl_IO ($x,$y) @const => (IOC_VOID | (($x)<<8)|y); +macro ulong @ioctl_IOR ($x,$y,$Type) @const => (IOC_OUT |(($Type.sizeof&IOCPARM_MASK)<<16)|(($x)<<8)|$y); +macro ulong @ioctl_IOW ($x,$y,$Type) @const => (IOC_IN |(($Type.sizeof&IOCPARM_MASK)<<16)|(($x)<<8)|$y); +/* this should be _IORW, but stdio got there first */ +macro ulong @ioctl_IOWR ($x,$y,$Type) @const => (IOC_INOUT|(($Type.sizeof&IOCPARM_MASK)<<16)|(($x)<<8)|$y); +macro ulong @ioctl_ION ($x,$y,$n) @const => (IOC_INOUT|((($n)&IOCPARM_MASK)<<16)|(($x)<<8)|y); + +// https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/sys/epoll.h +const EPOLL_IOC_TYPE = 0x8A; +const EPIOCSPARAMS = @ioctl_IOW(EPOLL_IOC_TYPE, 0x01, EpollParams); +const EPIOCGPARAMS = @ioctl_IOR(EPOLL_IOC_TYPE, 0x02, EpollParams); + +<* +* Creates an epoll instance. Returns an fd for the new instance. +* The "size" parameter is a hint specifying the number of file +* descriptors to be associated with the new instance. +* The fd returned by epoll_create() should be closed with close(). +*> +extern fn int epoll_create(int); + +<* +* Same as epoll_create but with an FLAGS parameter. +* The unused SIZE parameter has been dropped. +*> +extern fn int epoll_create1(int); + +<* +* Manipulate an epoll instance "epfd". Returns 0 in case of success, +* -1 in case of error ( the "errno" variable will contain the +* specific error code ) The "op" parameter is one of the EPOLL_CTL_* +* constants defined above. The "fd" parameter is the target of the +* operation. The "event" parameter describes which events the caller +* is interested in and any associated user data. +*> +extern fn int epoll_ctl (int, int, int, EpollEvent*); + +<* +* Wait for events on an epoll instance "epfd". Returns the number of +* triggered events returned in "events" buffer. Or -1 in case of +* error with the "errno" variable set to the specific error code. The +* "events" parameter is a buffer that will contain triggered +* events. The "maxevents" is the maximum number of events to be +* returned ( usually size of "events" ). The "timeout" parameter +* specifies the maximum wait time in milliseconds (-1 == infinite). +*> +extern fn int epoll_wait (int, EpollEvent*, int, int); + +<* +* Same as epoll_wait, but the thread's signal mask is temporarily +* and atomically replaced with the one provided as parameter. +*> +extern fn int epoll_pwait (int, EpollEvent*, int, int, Sigset_t*); + +<* +* Same as epoll_pwait, but the timeout as a timespec. +*> +extern fn int epoll_pwait2 (int, EpollEvent*, int, TimeSpec*, Sigset_t*); diff --git a/lib/std/os/linux/linux.c3 b/lib/std/os/linux/linux.c3 index f5ed43fe7..39d98ba6c 100644 --- a/lib/std/os/linux/linux.c3 +++ b/lib/std/os/linux/linux.c3 @@ -1,5 +1,34 @@ module std::os::linux @if(env::LINUX); -import libc, std::os, std::io, std::collections::list; +import libc, std::os, std::io, std::collections::list, std::net::os; + +// https://man7.org/linux/man-pages/man3/inet_ntop.3.html +extern fn char** inet_ntop(int, void*, char*, Socklen_t); + +// https://linux.die.net/man/3/ntohs +<* +* The htonl() function converts the unsigned integer hostlong from host byte order to network byte order. +*> +extern fn uint htonl(uint hostlong); +<* +* The htons() function converts the unsigned short integer hostshort from host byte order to network byte order. +*> +extern fn ushort htons(ushort hostshort); +<* +* The ntohl() function converts the unsigned integer netlong from network byte order to host byte order. +*> +extern fn uint ntohl(uint netlong); +<* +* The ntohs() function converts the unsigned short integer netshort from network byte order to host byte order. +*> +extern fn ushort ntohs(ushort netshort); + +// https://man7.org/linux/man-pages/man3/bzero.3.html +<* +* The bzero() function erases the data in the n bytes of the memory +* starting at the location pointed to by s, by writing zeros (bytes +* containing '\0') to that area. +*> +extern fn void bzero(char*, usz); extern fn isz readlink(ZString path, char* buf, usz bufsize); diff --git a/test/unit/stdlib/os/linux_epoll.c3 b/test/unit/stdlib/os/linux_epoll.c3 new file mode 100644 index 000000000..6e0b01dad --- /dev/null +++ b/test/unit/stdlib/os/linux_epoll.c3 @@ -0,0 +1,123 @@ +module std::os::linux @test @if (env::LINUX); +import std::net::os; +import std::os::posix; +import libc; + +fn void test_epoll_create() +{ + // Test epoll_create with valid size + int epoll_fd = epoll_create(1); + assert(epoll_fd >= 0, "epoll_create failed"); + libc::close(epoll_fd); + + // Test epoll_create with size 0 (should work on Linux) + epoll_fd = epoll_create1(0); + assert(epoll_fd >= 0, "epoll_create1 failed"); + libc::close(epoll_fd); + + // Test epoll_create with negative size (should fail) + epoll_fd = epoll_create(-1); + assert(epoll_fd == -1, "epoll_create with negative size should fail"); +} + +fn void test_epoll_ctl() +{ + int epoll_fd = epoll_create(1); + assert(epoll_fd >= 0, "epoll_create failed"); + + // Create a socket to monitor + int sock_fd = os::socket(os::AF_INET, os::SOCK_STREAM, 0); + assert(sock_fd >= 0, "socket creation failed"); + + EpollEvent event; + event.events = EPOLLIN; + event.data.fd = sock_fd; + + // Test EPOLL_CTL_ADD + int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event); + assert(ret == 0, "EPOLL_CTL_ADD failed"); + + // Test EPOLL_CTL_MOD + event.events = EPOLLIN | EPOLLOUT; + ret = epoll_ctl(epoll_fd, EPOLL_CTL_MOD, sock_fd, &event); + assert(ret == 0, "EPOLL_CTL_MOD failed"); + + // Test EPOLL_CTL_DEL + ret = epoll_ctl(epoll_fd, EPOLL_CTL_DEL, sock_fd, null); + assert(ret == 0, "EPOLL_CTL_DEL failed"); + + // Test invalid operations + ret = epoll_ctl(epoll_fd, -1, sock_fd, &event); // invalid op + assert(ret == -1, "Invalid operation should fail"); + + libc::close(sock_fd); + libc::close(epoll_fd); +} + +fn void test_epoll_edge_triggered() +{ + int epoll_fd = epoll_create(1); + assert(epoll_fd >= 0, "epoll_create failed"); + + int[2] pipe_fds; + assert(posix::pipe(&pipe_fds) == 0, "pipe creation failed"); + + // Set up edge-triggered monitoring + EpollEvent event; + event.events = EPOLLIN | EPOLLET; + event.data.fd = pipe_fds[0]; + assert(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pipe_fds[0], &event) == 0, + "epoll_ctl add failed"); + + // Write to pipe + char[] buf = "test"; + assert(libc::write(pipe_fds[1], buf, buf.len) == buf.len, + "write to pipe failed"); + + // First epoll_wait should get the event + EpollEvent[10] events; + int num_events = epoll_wait(epoll_fd, &events[0], events.len, 0); + assert(num_events == 1, "epoll_wait should return 1 event"); + + // Second epoll_wait shouldn't get the same event (edge-triggered behavior) + num_events = epoll_wait(epoll_fd, &events[0], events.len, 0); + assert(num_events == 0, "epoll_wait should not return event again for edge-triggered"); + + libc::close(pipe_fds[0]); + libc::close(pipe_fds[1]); + libc::close(epoll_fd); +} + +fn void test_epoll_level_triggered() +{ + int epoll_fd = epoll_create(1); + assert(epoll_fd >= 0, "epoll_create failed"); + + int[2] pipe_fds; + assert(posix::pipe(&pipe_fds) == 0, "pipe creation failed"); + + // Set up level-triggered monitoring + EpollEvent event; + event.events = EPOLLIN; // Default is level-triggered + event.data.fd = pipe_fds[0]; + assert(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pipe_fds[0], &event) == 0, + "epoll_ctl add failed"); + + // Write to pipe + char[] buf = "test"; + assert(libc::write(pipe_fds[1], buf, buf.len) == buf.len, + "write to pipe failed"); + + // First epoll_wait should get the event + EpollEvent[10] events; + int num_events = epoll_wait(epoll_fd, &events[0], events.len, 0); + assert(num_events == 1, "epoll_wait should return 1 event"); + + // Second epoll_wait should get the same event again (level-triggered behavior) + num_events = epoll_wait(epoll_fd, &events[0], events.len, 0); + assert(num_events == 1, "epoll_wait should return event again for level-triggered"); + + libc::close(pipe_fds[0]); + libc::close(pipe_fds[1]); + libc::close(epoll_fd); +}