Initial commit: phases 1-3 du portage Wayland Rust pour Redox OS

Plan directeur 14 phases / 5 ans (REDOX_COSMIC_XWAYLAND_RS_PLAN.md).

Phase 1 — Audit Redox (docs/existing-redox-gui.md, 486 lignes) :
- Orbital, graphics-ipc (API DRM compatible Linux subset KMS), inputd, vesad
- relibc support : AF_UNIX, SCM_RIGHTS, shm_open, mmap, poll
- 3 manques identifiés : memfd_create, keymap XKB, AT-SPI

Phase 2 — Validation primitives sur Redox via redoxer (5 tests + 1 POC) :
- test-unix-socket : SOCK_STREAM Wayland-shaped roundtrip
- test-fd-passing : SCM_RIGHTS mono-process (artefact kernel)
- test-fd-passing-fork : SCM_RIGHTS multi-process (validation Wayland critique)
- test-shm-open : shm_open + mmap + persistance + unlink
- test-poll-multifd : poll() multiplexing + POLLHUP
- poc-pixels : datapath shm + SCM_RIGHTS bout en bout (10000 pixels ARGB)

Phase 3 — wayland-rs sur Redox (compile + runtime) :
- wayland-{scanner,backend,server,client} compilent pour x86_64-unknown-redox
  sans patch upstream (rustix supporte Redox via libc backend)
- test-handshake : server/client wl_registry handshake roundtrip
- test-shm-pipeline : pipeline complet (ListeningSocket Unix réel + fd passing
  via wl_shm.create_pool + wl_shm_pool + wl_buffer + wl_surface + commit +
  serveur lit pixels via fd reçu, validation pixel-perfect)

Verdict phase 3 : wayland-rs upstream est viable sur Redox out-of-the-box,
le port "Wayland sur Redox" est désormais un problème de compositor à écrire,
pas de stack à porter.

Prérequis build : redoxer (pas cargo direct, car CMSG_NXTHDR/CMSG_DATA
ne sont pas linkés autrement vers librelibc.a).

Leyoda 2026 – GPLv3
This commit is contained in:
Votre Nom 2026-05-08 17:41:55 +02:00
commit 53e6626231
21 changed files with 3676 additions and 0 deletions

View file

@ -0,0 +1,7 @@
[package]
name = "redox-wl-poc-pixels"
version = "0.1.0"
edition = "2021"
[dependencies]
libc = "0.2"

View file

@ -0,0 +1,310 @@
//! POC — preuve par les pixels.
//!
//! Démontre concrètement que le datapath Wayland fonctionne sur Redox :
//! - un CLIENT crée une zone shm, dessine un pattern ARGB déterministe
//! - il envoie le fd au SERVEUR via Unix socket + SCM_RIGHTS
//! - le SERVEUR mappe la zone, vérifie chaque pixel, écrit un PPM
//!
//! C'est exactement ce que fait wl_shm + wl_buffer entre un client Wayland
//! et son compositor, sans le protocole Wayland lui-même. Si ce POC passe,
//! la phase 3 (port wayland-rs) peut démarrer en confiance.
//!
//! Mono-binaire qui fork() : le child fait le client, le parent le serveur.
use std::ffi::CString;
use std::io::{self, Write};
use std::mem::{self, MaybeUninit};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::process::ExitCode;
use std::ptr;
const W: usize = 200;
const H: usize = 200;
const PIXEL_BYTES: usize = 4;
const SHM_BYTES: usize = W * H * PIXEL_BYTES;
const SHM_NAME: &str = "/redox-wl-poc-pixels";
const PPM_OUTPUT: &str = "poc-shm-pixels-readback.ppm";
/// Pattern ARGB8888 déterministe : channel A = 0xFF, R = x, G = y, B = (x+y) mod 256.
fn expected_pixel(x: usize, y: usize) -> u32 {
let a: u32 = 0xFF;
let r: u32 = (x as u32) & 0xFF;
let g: u32 = (y as u32) & 0xFF;
let b: u32 = ((x + y) as u32) & 0xFF;
(a << 24) | (r << 16) | (g << 8) | b
}
fn errno_str() -> String {
let e = io::Error::last_os_error();
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
}
unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> {
let mut fds: [RawFd; 2] = [-1, -1];
if libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) != 0 {
return Err(format!("socketpair: {}", errno_str()));
}
Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])))
}
unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> {
let mut iov_buf = [b'P'; 1];
let mut iov = libc::iovec {
iov_base: iov_buf.as_mut_ptr() as *mut _,
iov_len: 1,
};
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
let mut cmsg_buf = vec![0u8; cmsg_space];
let mut msg: libc::msghdr = mem::zeroed();
msg.msg_iov = &mut iov as *mut _;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
msg.msg_controllen = cmsg_space as _;
let cmsg = libc::CMSG_FIRSTHDR(&msg);
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::<RawFd>() as u32) as _;
let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd;
ptr::write_unaligned(data_ptr, fd_to_send);
let n = libc::sendmsg(socket, &msg, 0);
if n < 0 {
return Err(format!("sendmsg: {}", errno_str()));
}
Ok(())
}
unsafe fn recv_fd_from(socket: RawFd) -> Result<OwnedFd, String> {
let mut iov_buf = [0u8; 1];
let mut iov = libc::iovec {
iov_base: iov_buf.as_mut_ptr() as *mut _,
iov_len: 1,
};
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
let mut cmsg_buf = vec![MaybeUninit::<u8>::uninit(); cmsg_space];
let mut msg: libc::msghdr = mem::zeroed();
msg.msg_iov = &mut iov as *mut _;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
msg.msg_controllen = cmsg_space as _;
let n = libc::recvmsg(socket, &mut msg, 0);
if n < 0 {
return Err(format!("recvmsg: {}", errno_str()));
}
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
return Err("no cmsg received".into());
}
if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
return Err("unexpected cmsg type".into());
}
let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd;
let received_fd = ptr::read_unaligned(data_ptr);
Ok(OwnedFd::from_raw_fd(received_fd))
}
unsafe fn shm_create_and_map(name: &str, size: usize) -> Result<(OwnedFd, *mut u8), String> {
let cname = CString::new(name).unwrap();
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600);
if fd < 0 {
return Err(format!("shm_open: {}", errno_str()));
}
if libc::ftruncate(fd, size as _) != 0 {
libc::close(fd);
return Err(format!("ftruncate: {}", errno_str()));
}
let p = libc::mmap(
ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
0,
);
if p == libc::MAP_FAILED {
libc::close(fd);
return Err(format!("mmap (create): {}", errno_str()));
}
Ok((OwnedFd::from_raw_fd(fd), p as *mut u8))
}
unsafe fn mmap_existing(fd: RawFd, size: usize) -> Result<*mut u8, String> {
let p = libc::mmap(
ptr::null_mut(),
size,
libc::PROT_READ,
libc::MAP_SHARED,
fd,
0,
);
if p == libc::MAP_FAILED {
return Err(format!("mmap (recv): {}", errno_str()));
}
Ok(p as *mut u8)
}
unsafe fn child_main(child_sock: OwnedFd) -> ! {
eprintln!("[poc CLIENT pid={}] start", libc::getpid());
// Crée la zone shm
let (shm_fd, map) = match shm_create_and_map(SHM_NAME, SHM_BYTES) {
Ok(v) => v,
Err(e) => {
eprintln!("[poc CLIENT] FAIL shm_create_and_map: {e}");
libc::_exit(2);
}
};
eprintln!(
"[poc CLIENT] shm '{}' size={} mapped at {:p} fd={}",
SHM_NAME, SHM_BYTES, map, shm_fd.as_raw_fd()
);
// Dessine le pattern ARGB déterministe
let pixels = std::slice::from_raw_parts_mut(map as *mut u32, W * H);
for y in 0..H {
for x in 0..W {
pixels[y * W + x] = expected_pixel(x, y);
}
}
eprintln!(
"[poc CLIENT] painted {}x{} ARGB pattern (a=0xFF, r=x, g=y, b=(x+y)&0xFF)",
W, H
);
// Envoie le fd au serveur
if let Err(e) = send_fd_over(child_sock.as_raw_fd(), shm_fd.as_raw_fd()) {
eprintln!("[poc CLIENT] FAIL sendmsg: {e}");
libc::_exit(3);
}
eprintln!("[poc CLIENT] sent shm fd via SCM_RIGHTS");
// On peut munmap + close côté client : le serveur a son propre ref via SCM_RIGHTS
libc::munmap(map as *mut _, SHM_BYTES);
drop(shm_fd);
eprintln!("[poc CLIENT] done, exit 0");
libc::_exit(0);
}
unsafe fn parent_main(parent_sock: OwnedFd, child_pid: libc::pid_t) -> Result<(), String> {
eprintln!("[poc SERVER pid={}] waiting for fd from child {}", libc::getpid(), child_pid);
let received = recv_fd_from(parent_sock.as_raw_fd())?;
let recv_raw = received.as_raw_fd();
eprintln!("[poc SERVER] received fd={recv_raw}");
let map = mmap_existing(recv_raw, SHM_BYTES)?;
eprintln!("[poc SERVER] mmap'd received fd at {:p}", map);
// Vérifie chaque pixel
let pixels = std::slice::from_raw_parts(map as *const u32, W * H);
let mut mismatches: Vec<(usize, usize, u32, u32)> = Vec::new();
for y in 0..H {
for x in 0..W {
let got = pixels[y * W + x];
let want = expected_pixel(x, y);
if got != want {
if mismatches.len() < 5 {
mismatches.push((x, y, got, want));
}
}
}
}
if !mismatches.is_empty() {
for (x, y, got, want) in &mismatches {
eprintln!(
"[poc SERVER] MISMATCH at ({x},{y}): got {got:#010x} want {want:#010x}"
);
}
return Err(format!("{} pixel mismatches over {}", mismatches.len(), W * H));
}
eprintln!(
"[poc SERVER] all {} pixels match expected pattern",
W * H
);
// Bonus : dump en PPM pour visualisation (P6 binaire RGB)
match write_ppm(PPM_OUTPUT, pixels, W, H) {
Ok(()) => eprintln!("[poc SERVER] PPM dump written to {PPM_OUTPUT}"),
Err(e) => eprintln!("[poc SERVER] WARN: PPM dump failed: {e}"),
}
libc::munmap(map as *mut _, SHM_BYTES);
drop(received);
// Reap child
let mut status: libc::c_int = 0;
let r = libc::waitpid(child_pid, &mut status, 0);
if r < 0 {
return Err(format!("waitpid: {}", errno_str()));
}
if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 {
return Err(format!(
"child exited abnormally: status={status}"
));
}
eprintln!("[poc SERVER] child reaped cleanly");
// Cleanup shm
let cname = CString::new(SHM_NAME).unwrap();
libc::shm_unlink(cname.as_ptr());
Ok(())
}
fn write_ppm(path: &str, pixels: &[u32], w: usize, h: usize) -> io::Result<()> {
let mut f = std::fs::File::create(path)?;
write!(f, "P6\n{w} {h}\n255\n")?;
let mut row = vec![0u8; w * 3];
for y in 0..h {
for x in 0..w {
let argb = pixels[y * w + x];
let r = ((argb >> 16) & 0xFF) as u8;
let g = ((argb >> 8) & 0xFF) as u8;
let b = (argb & 0xFF) as u8;
row[x * 3] = r;
row[x * 3 + 1] = g;
row[x * 3 + 2] = b;
}
f.write_all(&row)?;
}
Ok(())
}
fn run() -> Result<(), String> {
println!(
"[poc] preuve par les pixels: client {}x{} ARGB → SCM_RIGHTS → serveur",
W, H
);
unsafe {
let (sock_parent, sock_child) = make_socketpair()?;
let pid = libc::fork();
if pid < 0 {
return Err(format!("fork: {}", errno_str()));
}
if pid == 0 {
drop(sock_parent);
child_main(sock_child);
}
drop(sock_child);
parent_main(sock_parent, pid)?;
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
println!("[poc] PASS: full Wayland-like datapath validated on Redox");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("[poc] FAIL: {e}");
ExitCode::FAILURE
}
}
}

View file

@ -0,0 +1,7 @@
[package]
name = "redox-wl-test-fd-passing-fork"
version = "0.1.0"
edition = "2021"
[dependencies]
libc = "0.2"

View file

@ -0,0 +1,215 @@
//! Test 2b — fd passing via SCM_RIGHTS, multi-process via fork().
//!
//! Mono-process test (test-02) revealed the kernel reuses fd numbers and
//! does not duplicate the file table entry. This test validates whether
//! SCM_RIGHTS works correctly *across* a process boundary, which is the
//! actual Wayland use case.
//!
//! Flow:
//! 1. Parent opens a tmp file with a marker
//! 2. socketpair(AF_UNIX, SOCK_STREAM)
//! 3. fork()
//! 4. Parent: closes child end, sendmsg with the tmp_fd
//! 5. Child: closes parent end, recvmsg, reads via received fd, exits with status 0 if marker matches
//! 6. Parent: waitpid, returns child's exit status
use std::ffi::CString;
use std::io;
use std::mem::{self, MaybeUninit};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::process::ExitCode;
use std::ptr;
const MARKER: &[u8] = b"REDOX-FORK-FD-PASSING-OK-2026";
fn errno_str() -> String {
let e = io::Error::last_os_error();
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
}
unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> {
let mut fds: [RawFd; 2] = [-1, -1];
if libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) != 0 {
return Err(format!("socketpair: {}", errno_str()));
}
Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])))
}
unsafe fn open_tmp_with_marker() -> Result<OwnedFd, String> {
let path = CString::new("/tmp/test-02b-fd-passing-fork.bin").unwrap();
let fd = libc::open(
path.as_ptr(),
libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC,
0o600,
);
if fd < 0 {
return Err(format!("open tmp: {}", errno_str()));
}
let n = libc::write(fd, MARKER.as_ptr() as *const _, MARKER.len());
if n != MARKER.len() as isize {
return Err(format!("write tmp: {}", errno_str()));
}
libc::lseek(fd, 0, libc::SEEK_SET);
Ok(OwnedFd::from_raw_fd(fd))
}
unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> {
let mut iov_buf = [b'X'; 1];
let mut iov = libc::iovec {
iov_base: iov_buf.as_mut_ptr() as *mut _,
iov_len: 1,
};
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
let mut cmsg_buf = vec![0u8; cmsg_space];
let mut msg: libc::msghdr = mem::zeroed();
msg.msg_iov = &mut iov as *mut _;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
msg.msg_controllen = cmsg_space as _;
let cmsg = libc::CMSG_FIRSTHDR(&msg);
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::<RawFd>() as u32) as _;
let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd;
ptr::write_unaligned(data_ptr, fd_to_send);
let n = libc::sendmsg(socket, &msg, 0);
if n < 0 {
return Err(format!("sendmsg: {}", errno_str()));
}
Ok(())
}
unsafe fn recv_fd_from(socket: RawFd) -> Result<OwnedFd, String> {
let mut iov_buf = [0u8; 1];
let mut iov = libc::iovec {
iov_base: iov_buf.as_mut_ptr() as *mut _,
iov_len: 1,
};
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
let mut cmsg_buf = vec![MaybeUninit::<u8>::uninit(); cmsg_space];
let mut msg: libc::msghdr = mem::zeroed();
msg.msg_iov = &mut iov as *mut _;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
msg.msg_controllen = cmsg_space as _;
let n = libc::recvmsg(socket, &mut msg, 0);
if n < 0 {
return Err(format!("recvmsg: {}", errno_str()));
}
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
return Err("no cmsg".into());
}
if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
return Err(format!(
"unexpected cmsg level={} type={}",
(*cmsg).cmsg_level,
(*cmsg).cmsg_type
));
}
let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd;
let received_fd = ptr::read_unaligned(data_ptr);
Ok(OwnedFd::from_raw_fd(received_fd))
}
unsafe fn child_main(child_sock: OwnedFd) -> ! {
let raw_sock = child_sock.as_raw_fd();
eprintln!("[test-02b CHILD pid={}] recvmsg on sock {}", libc::getpid(), raw_sock);
let received = match recv_fd_from(raw_sock) {
Ok(fd) => fd,
Err(e) => {
eprintln!("[test-02b CHILD] recvmsg fail: {e}");
libc::_exit(2);
}
};
let recv_raw = received.as_raw_fd();
eprintln!("[test-02b CHILD] received fd={recv_raw}");
libc::lseek(recv_raw, 0, libc::SEEK_SET);
let mut readback = vec![0u8; MARKER.len()];
let n = libc::read(recv_raw, readback.as_mut_ptr() as *mut _, readback.len());
if n != MARKER.len() as isize {
eprintln!(
"[test-02b CHILD] read returned {n}, errno={}",
io::Error::last_os_error().raw_os_error().unwrap_or(0)
);
libc::_exit(3);
}
if readback != MARKER {
eprintln!(
"[test-02b CHILD] marker mismatch: {:?}",
String::from_utf8_lossy(&readback)
);
libc::_exit(4);
}
eprintln!("[test-02b CHILD] marker matches, exit 0");
libc::_exit(0);
}
fn run() -> Result<(), String> {
println!("[test-02b] fd passing via SCM_RIGHTS across fork()");
unsafe {
let (sock_parent, sock_child) = make_socketpair()?;
let tmp_fd = open_tmp_with_marker()?;
println!("[test-02b PARENT] tmp fd={}, sockets {}/{}",
tmp_fd.as_raw_fd(), sock_parent.as_raw_fd(), sock_child.as_raw_fd());
let pid = libc::fork();
if pid < 0 {
return Err(format!("fork: {}", errno_str()));
}
if pid == 0 {
// Child: drop tmp_fd, drop parent socket, run child_main on its own socket
drop(tmp_fd);
drop(sock_parent);
child_main(sock_child);
}
// Parent path
drop(sock_child);
println!("[test-02b PARENT] forked child pid={pid}, sending fd");
send_fd_over(sock_parent.as_raw_fd(), tmp_fd.as_raw_fd())?;
drop(tmp_fd);
drop(sock_parent);
let mut status: libc::c_int = 0;
let r = libc::waitpid(pid, &mut status, 0);
if r < 0 {
return Err(format!("waitpid: {}", errno_str()));
}
let exited = libc::WIFEXITED(status);
let code = libc::WEXITSTATUS(status);
println!("[test-02b PARENT] child reaped: exited={exited} code={code}");
if !exited {
return Err("child did not exit normally".into());
}
if code != 0 {
return Err(format!("child exited with status {code}"));
}
// Cleanup tmp file
let path = std::ffi::CString::new("/tmp/test-02b-fd-passing-fork.bin").unwrap();
libc::unlink(path.as_ptr());
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
println!("[test-02b] PASS: fd passed across fork() correctly");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("[test-02b] FAIL: {e}");
ExitCode::FAILURE
}
}
}

View file

@ -0,0 +1,7 @@
[package]
name = "redox-wl-test-fd-passing"
version = "0.1.0"
edition = "2021"
[dependencies]
libc = "0.2"

View file

@ -0,0 +1,195 @@
//! Test 2 — fd passing via sendmsg/recvmsg with SCM_RIGHTS.
//!
//! This is THE critical primitive for Wayland: clients pass shm fds
//! (and dmabuf fds) to the compositor through this mechanism.
//!
//! relibc Redox (platform/redox/socket.rs:272,447,835,924) declares
//! support — this test validates the actual behaviour.
//!
//! Flow:
//! 1. socketpair(AF_UNIX, SOCK_STREAM)
//! 2. open a tmp file, write a known marker into it
//! 3. sendmsg the fd via SCM_RIGHTS through socket A
//! 4. recvmsg on socket B, extract the fd
//! 5. read from the received fd, check marker matches
//!
//! Single-process test (no fork needed): we send the fd to ourselves
//! through the socketpair to validate the mechanism works end-to-end.
use std::ffi::CString;
use std::io;
use std::mem::{self, MaybeUninit};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::process::ExitCode;
use std::ptr;
const MARKER: &[u8] = b"REDOX-FD-PASSING-OK-2026";
fn errno_str() -> String {
let e = io::Error::last_os_error();
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
}
unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> {
let mut fds: [RawFd; 2] = [-1, -1];
let r = libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr());
if r != 0 {
return Err(format!("socketpair: {}", errno_str()));
}
Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])))
}
unsafe fn open_tmp_with_marker() -> Result<OwnedFd, String> {
let path = CString::new("/tmp/test-02-fd-passing.bin").unwrap();
let fd = libc::open(
path.as_ptr(),
libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC,
0o600,
);
if fd < 0 {
return Err(format!("open tmp: {}", errno_str()));
}
let n = libc::write(fd, MARKER.as_ptr() as *const _, MARKER.len());
if n != MARKER.len() as isize {
return Err(format!("write tmp: {}", errno_str()));
}
libc::lseek(fd, 0, libc::SEEK_SET);
Ok(OwnedFd::from_raw_fd(fd))
}
unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> {
let mut iov_buf = [b'X'; 1];
let mut iov = libc::iovec {
iov_base: iov_buf.as_mut_ptr() as *mut _,
iov_len: 1,
};
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
let mut cmsg_buf = vec![0u8; cmsg_space];
let mut msg: libc::msghdr = mem::zeroed();
msg.msg_iov = &mut iov as *mut _;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
msg.msg_controllen = cmsg_space as _;
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
return Err("CMSG_FIRSTHDR null".into());
}
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::<RawFd>() as u32) as _;
let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd;
ptr::write_unaligned(data_ptr, fd_to_send);
let n = libc::sendmsg(socket, &msg, 0);
if n < 0 {
return Err(format!("sendmsg: {}", errno_str()));
}
println!("[test-02] sendmsg sent {n} bytes + 1 fd via SCM_RIGHTS");
Ok(())
}
unsafe fn recv_fd_from(socket: RawFd) -> Result<OwnedFd, String> {
let mut iov_buf = [0u8; 1];
let mut iov = libc::iovec {
iov_base: iov_buf.as_mut_ptr() as *mut _,
iov_len: 1,
};
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
let mut cmsg_buf = vec![MaybeUninit::<u8>::uninit(); cmsg_space];
let mut msg: libc::msghdr = mem::zeroed();
msg.msg_iov = &mut iov as *mut _;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
msg.msg_controllen = cmsg_space as _;
let n = libc::recvmsg(socket, &mut msg, 0);
if n < 0 {
return Err(format!("recvmsg: {}", errno_str()));
}
println!("[test-02] recvmsg got {n} bytes + control of {} bytes", msg.msg_controllen);
let cmsg = libc::CMSG_FIRSTHDR(&msg);
if cmsg.is_null() {
return Err("no cmsg in received message".into());
}
if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
return Err(format!(
"unexpected cmsg level={} type={}",
(*cmsg).cmsg_level,
(*cmsg).cmsg_type
));
}
let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd;
let received_fd = ptr::read_unaligned(data_ptr);
if received_fd < 0 {
return Err(format!("received negative fd: {received_fd}"));
}
Ok(OwnedFd::from_raw_fd(received_fd))
}
fn run() -> Result<(), String> {
println!("[test-02] fd passing via SCM_RIGHTS");
unsafe {
let (sock_a, sock_b) = make_socketpair()?;
let tmp_fd = open_tmp_with_marker()?;
println!("[test-02] tmp fd = {}", tmp_fd.as_raw_fd());
// Strategy: send a dup'd fd, then close BOTH the original and the dup.
// If SCM_RIGHTS works per POSIX, the received fd is an independent
// reference to the open file and must remain readable.
let dup_fd = libc::dup(tmp_fd.as_raw_fd());
if dup_fd < 0 {
return Err(format!("dup: {}", errno_str()));
}
println!("[test-02] dup'd tmp_fd ({}) -> {dup_fd}", tmp_fd.as_raw_fd());
send_fd_over(sock_a.as_raw_fd(), dup_fd)?;
let received = recv_fd_from(sock_b.as_raw_fd())?;
let recv_raw = received.as_raw_fd();
println!("[test-02] received fd = {recv_raw}");
// Close BOTH the original and the dup. If SCM_RIGHTS gave us a real
// duplicate, the received fd must survive.
let orig_raw = tmp_fd.as_raw_fd();
drop(tmp_fd);
libc::close(dup_fd);
println!("[test-02] closed original fd ({orig_raw}) and dup ({dup_fd}); reading via received only");
libc::lseek(recv_raw, 0, libc::SEEK_SET);
let mut readback = vec![0u8; MARKER.len()];
let n = libc::read(recv_raw, readback.as_mut_ptr() as *mut _, readback.len());
if n != MARKER.len() as isize {
return Err(format!("read from received fd: only {n} bytes (errno {})",
io::Error::last_os_error().raw_os_error().unwrap_or(0)));
}
if readback != MARKER {
return Err(format!("marker mismatch: got {:?}", String::from_utf8_lossy(&readback)));
}
println!("[test-02] read {} bytes via received fd: marker matches", n);
// Cleanup tmp file
let path = std::ffi::CString::new("/tmp/test-02-fd-passing.bin").unwrap();
libc::unlink(path.as_ptr());
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
println!("[test-02] PASS: fd passing works, received fd reads correct marker");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("[test-02] FAIL: {e}");
ExitCode::FAILURE
}
}
}

View file

@ -0,0 +1,9 @@
[package]
name = "redox-wl-test-handshake"
version = "0.1.0"
edition = "2021"
[dependencies]
wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false }
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }

View file

@ -0,0 +1,200 @@
//! Test 5 — Wayland handshake server/client sur Redox.
//!
//! Valide que wayland-rs (server + client + backend) ne se contente pas de
//! compiler pour Redox mais fonctionne réellement à l'exécution :
//!
//! 1. Serveur Wayland local + globals (wl_compositor, wl_shm)
//! 2. socketpair AF_UNIX, on insère le côté serveur via insert_client
//! 3. Client Wayland connecté sur l'autre côté via WAYLAND_SOCKET fd env
//! 4. Client fait wl_display.get_registry, dispatch
//! 5. On vérifie que les globals attendus arrivent côté client
//!
//! Si ça passe, le port wayland-rs sur Redox est validé runtime, pas juste
//! compile-time.
use std::os::unix::net::UnixStream;
use std::process::ExitCode;
use std::sync::Arc;
use wayland_client::{
Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue,
backend::Backend as ClientBackend,
protocol::wl_registry,
};
use wayland_server::{
Display as ServerDisplay, GlobalDispatch, New,
backend::ClientData,
protocol::{wl_compositor, wl_shm},
};
// ---- Server side ----
struct ServerState;
#[derive(Debug)]
struct DumbClientData;
impl ClientData for DumbClientData {}
impl GlobalDispatch<wl_compositor::WlCompositor, ()> for ServerState {
fn bind(
_state: &mut Self,
_handle: &wayland_server::DisplayHandle,
_client: &wayland_server::Client,
_resource: New<wl_compositor::WlCompositor>,
_data: &(),
_data_init: &mut wayland_server::DataInit<'_, Self>,
) {
}
}
impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for ServerState {
fn request(
_state: &mut Self,
_client: &wayland_server::Client,
_resource: &wl_compositor::WlCompositor,
_request: wl_compositor::Request,
_data: &(),
_dh: &wayland_server::DisplayHandle,
_data_init: &mut wayland_server::DataInit<'_, Self>,
) {
}
}
impl GlobalDispatch<wl_shm::WlShm, ()> for ServerState {
fn bind(
_state: &mut Self,
_handle: &wayland_server::DisplayHandle,
_client: &wayland_server::Client,
_resource: New<wl_shm::WlShm>,
_data: &(),
_data_init: &mut wayland_server::DataInit<'_, Self>,
) {
}
}
impl wayland_server::Dispatch<wl_shm::WlShm, ()> for ServerState {
fn request(
_state: &mut Self,
_client: &wayland_server::Client,
_resource: &wl_shm::WlShm,
_request: wl_shm::Request,
_data: &(),
_dh: &wayland_server::DisplayHandle,
_data_init: &mut wayland_server::DataInit<'_, Self>,
) {
}
}
// ---- Client side ----
#[derive(Default, Debug)]
struct ClientState {
globals: Vec<(u32, String, u32)>,
done: bool,
}
impl ClientDispatch<wl_registry::WlRegistry, ()> for ClientState {
fn event(
state: &mut Self,
_registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_data: &(),
_conn: &ClientConnection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
println!("[test-05 CLIENT] global #{name}: {interface} v{version}");
state.globals.push((name, interface, version));
}
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
println!("[test-05] wayland-rs handshake server/client on Redox");
// -- Server setup --
let mut server_display: ServerDisplay<ServerState> = ServerDisplay::new()?;
let mut dh = server_display.handle();
dh.create_global::<ServerState, wl_compositor::WlCompositor, _>(5, ());
dh.create_global::<ServerState, wl_shm::WlShm, _>(1, ());
println!("[test-05 SERVER] created wl_compositor v5 + wl_shm v1 globals");
// -- Socket linking --
let (s_server, s_client) = UnixStream::pair()?;
s_server.set_nonblocking(true)?;
let _client_handle = dh.insert_client(s_server, Arc::new(DumbClientData))?;
println!("[test-05] linked server side via insert_client");
// -- Client setup using the other end of the socketpair --
let client_backend = ClientBackend::connect(s_client)?;
let client_conn = ClientConnection::from_backend(client_backend);
let mut event_queue: EventQueue<ClientState> = client_conn.new_event_queue();
let qhandle = event_queue.handle();
let _registry = client_conn.display().get_registry(&qhandle, ());
println!("[test-05 CLIENT] called get_registry, dispatching...");
let mut server_state = ServerState;
let mut client_state = ClientState::default();
// -- Roundtrip: dispatch alternately until client has events --
for round in 0..10 {
// server side
server_display.dispatch_clients(&mut server_state)?;
server_display.flush_clients()?;
// client side
let _ = event_queue.flush();
let processed = event_queue.dispatch_pending(&mut client_state)?;
if processed > 0 {
println!("[test-05] round {round}: client processed {processed} event(s)");
}
if !client_state.globals.is_empty() {
client_state.done = true;
break;
}
// Block briefly waiting for incoming events on client side
match event_queue.prepare_read() {
Some(read_guard) => {
let _ = read_guard.read();
}
None => {}
}
}
if !client_state.done || client_state.globals.is_empty() {
return Err(format!(
"client did not receive globals (got {})",
client_state.globals.len()
)
.into());
}
let names: Vec<&str> = client_state.globals.iter().map(|(_, n, _)| n.as_str()).collect();
let has_compositor = names.contains(&"wl_compositor");
let has_shm = names.contains(&"wl_shm");
if !has_compositor || !has_shm {
return Err(format!("missing expected globals: got {:?}", names).into());
}
println!(
"[test-05] received {} global(s): {:?}",
client_state.globals.len(),
names
);
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
println!("[test-05] PASS: wayland-rs server/client handshake works on Redox");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("[test-05] FAIL: {e}");
ExitCode::FAILURE
}
}
}

View file

@ -0,0 +1,7 @@
[package]
name = "redox-wl-test-poll-multifd"
version = "0.1.0"
edition = "2021"
[dependencies]
libc = "0.2"

View file

@ -0,0 +1,120 @@
//! Test 4 — poll() over multiple fds.
//!
//! A Wayland compositor multiplexes many fds: the listening socket,
//! each connected client, the input device(s), idle timers, signals.
//! This test validates that poll() correctly demultiplexes.
//!
//! Setup: 3 pipes (a, b, c). We write into b only, then poll all 3.
//! Only fd_b should report POLLIN. We read it. Then we close a, expecting
//! POLLHUP on a-read side. Finally we test timeout-only behaviour.
use std::io;
use std::os::fd::RawFd;
use std::process::ExitCode;
fn errno_str() -> String {
let e = io::Error::last_os_error();
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
}
unsafe fn make_pipe() -> Result<(RawFd, RawFd), String> {
let mut fds: [RawFd; 2] = [-1, -1];
if libc::pipe(fds.as_mut_ptr()) != 0 {
return Err(format!("pipe: {}", errno_str()));
}
Ok((fds[0], fds[1]))
}
unsafe fn poll_revents(fds: &mut [libc::pollfd], timeout_ms: i32) -> Result<i32, String> {
let r = libc::poll(fds.as_mut_ptr(), fds.len() as _, timeout_ms);
if r < 0 {
return Err(format!("poll: {}", errno_str()));
}
Ok(r)
}
fn run() -> Result<(), String> {
println!("[test-04] poll() multi-fd multiplexing");
unsafe {
// 3 pipes; we'll only watch the read ends.
let (ra, wa) = make_pipe()?;
let (rb, wb) = make_pipe()?;
let (rc, wc) = make_pipe()?;
println!("[test-04] pipes: ra={ra} rb={rb} rc={rc}");
// ---- Subtest A: timeout-only, no fd ready ----
let mut pfds = [
libc::pollfd { fd: ra, events: libc::POLLIN, revents: 0 },
libc::pollfd { fd: rb, events: libc::POLLIN, revents: 0 },
libc::pollfd { fd: rc, events: libc::POLLIN, revents: 0 },
];
let n = poll_revents(&mut pfds, 50)?;
if n != 0 {
return Err(format!("expected timeout (0), got {n}"));
}
println!("[test-04] A: timeout 50ms with no data → poll returned 0 OK");
// ---- Subtest B: write into pipe B only ----
let payload = b"X";
let n = libc::write(wb, payload.as_ptr() as *const _, 1);
if n != 1 {
return Err(format!("write to wb: {}", errno_str()));
}
for p in pfds.iter_mut() { p.revents = 0; }
let n = poll_revents(&mut pfds, 1000)?;
if n != 1 {
return Err(format!("expected 1 ready fd, got {n}"));
}
let ready: Vec<_> = pfds.iter().enumerate()
.filter(|(_, p)| p.revents & libc::POLLIN != 0)
.map(|(i, _)| i)
.collect();
if ready != [1] {
return Err(format!("expected only index 1 (rb) ready, got {ready:?}"));
}
println!("[test-04] B: poll detected only rb (index 1) ready, as expected");
let mut buf = [0u8; 1];
let r = libc::read(rb, buf.as_mut_ptr() as *mut _, 1);
if r != 1 || buf[0] != b'X' {
return Err(format!("read rb: r={r} buf={:?}", buf));
}
println!("[test-04] B: read 1 byte 'X' from rb OK");
// ---- Subtest C: close write end of A, expect HUP on read end ----
libc::close(wa);
for p in pfds.iter_mut() { p.revents = 0; }
let n = poll_revents(&mut pfds, 100)?;
if n < 1 {
return Err(format!("expected at least 1 ready (POLLHUP on ra), got {n}"));
}
let hup_a = pfds[0].revents & (libc::POLLHUP | libc::POLLIN) != 0;
if !hup_a {
return Err(format!("expected HUP/IN on ra, revents={:#x}", pfds[0].revents));
}
println!("[test-04] C: closing wa caused POLLHUP/POLLIN on ra (revents={:#x})",
pfds[0].revents);
// Cleanup
libc::close(ra);
libc::close(rb); libc::close(wb);
libc::close(rc); libc::close(wc);
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
println!("[test-04] PASS: poll() multiplexes correctly across fds and detects HUP");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("[test-04] FAIL: {e}");
ExitCode::FAILURE
}
}
}

View file

@ -0,0 +1,7 @@
[package]
name = "redox-wl-test-shm-open"
version = "0.1.0"
edition = "2021"
[dependencies]
libc = "0.2"

View file

@ -0,0 +1,142 @@
//! Test 3 — shm_open + mmap MAP_SHARED.
//!
//! Wayland clients use shm_open + ftruncate + mmap to create wl_shm pools.
//! The compositor then receives the fd via SCM_RIGHTS and mmaps the same region.
//!
//! relibc Redox maps shm_open("/foo") to open("/scheme/shm/foo").
//! This test validates:
//! 1. shm_open with O_RDWR | O_CREAT works
//! 2. ftruncate sets the size
//! 3. mmap MAP_SHARED returns a writable region
//! 4. writes are persistent: close + reopen + remap reads back the data
//! 5. shm_unlink removes the segment
//!
//! Two passes (write then read-back) simulate the producer/consumer pattern
//! used by Wayland: client writes pixels, compositor reads them.
use std::ffi::CString;
use std::io;
use std::os::fd::RawFd;
use std::process::ExitCode;
use std::ptr;
const SHM_NAME: &str = "/redox-wl-test-03";
const SIZE: usize = 4096;
fn errno_str() -> String {
let e = io::Error::last_os_error();
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
}
unsafe fn shm_open_rw_create(name: &str, mode: libc::mode_t) -> Result<RawFd, String> {
let cname = CString::new(name).unwrap();
let fd = libc::shm_open(
cname.as_ptr(),
libc::O_RDWR | libc::O_CREAT,
mode as _,
);
if fd < 0 {
return Err(format!("shm_open({name}): {}", errno_str()));
}
Ok(fd)
}
unsafe fn shm_open_rw(name: &str) -> Result<RawFd, String> {
let cname = CString::new(name).unwrap();
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0);
if fd < 0 {
return Err(format!("shm_open(reopen {name}): {}", errno_str()));
}
Ok(fd)
}
unsafe fn map_rw(fd: RawFd, size: usize) -> Result<*mut u8, String> {
let p = libc::mmap(
ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
0,
);
if p == libc::MAP_FAILED {
return Err(format!("mmap MAP_SHARED: {}", errno_str()));
}
Ok(p as *mut u8)
}
fn run() -> Result<(), String> {
println!("[test-03] shm_open + mmap MAP_SHARED");
unsafe {
// Phase 1: create + write
let fd1 = shm_open_rw_create(SHM_NAME, 0o600)?;
println!("[test-03] phase1: shm_open created, fd={fd1}");
if libc::ftruncate(fd1, SIZE as _) != 0 {
return Err(format!("ftruncate: {}", errno_str()));
}
let map1 = map_rw(fd1, SIZE)?;
println!("[test-03] phase1: mmap at {:p}", map1);
// Write a recognisable pattern (ARGB8888 simulation: 256 pixels of 0xDEADBEEF)
let pixels = std::slice::from_raw_parts_mut(map1 as *mut u32, SIZE / 4);
for (i, p) in pixels.iter_mut().enumerate() {
*p = 0xDEAD_BEEF_u32.wrapping_add(i as u32);
}
println!("[test-03] phase1: wrote {} u32 pixels", pixels.len());
if libc::munmap(map1 as *mut _, SIZE) != 0 {
return Err(format!("munmap phase1: {}", errno_str()));
}
libc::close(fd1);
// Phase 2: reopen + check pattern
let fd2 = shm_open_rw(SHM_NAME)?;
println!("[test-03] phase2: shm_open reopened, fd={fd2}");
let map2 = map_rw(fd2, SIZE)?;
println!("[test-03] phase2: mmap at {:p}", map2);
let pixels2 = std::slice::from_raw_parts(map2 as *const u32, SIZE / 4);
for (i, &p) in pixels2.iter().enumerate() {
let expected = 0xDEAD_BEEF_u32.wrapping_add(i as u32);
if p != expected {
libc::munmap(map2 as *mut _, SIZE);
libc::close(fd2);
let cname = CString::new(SHM_NAME).unwrap();
libc::shm_unlink(cname.as_ptr());
return Err(format!("pixel {i} mismatch: got {p:#x}, expected {expected:#x}"));
}
}
println!("[test-03] phase2: pattern verified across close/reopen");
if libc::munmap(map2 as *mut _, SIZE) != 0 {
return Err(format!("munmap phase2: {}", errno_str()));
}
libc::close(fd2);
// Cleanup
let cname = CString::new(SHM_NAME).unwrap();
if libc::shm_unlink(cname.as_ptr()) != 0 {
return Err(format!("shm_unlink: {}", errno_str()));
}
println!("[test-03] cleanup: shm_unlink OK");
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
println!("[test-03] PASS: shm_open/mmap/persistence/unlink all work");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("[test-03] FAIL: {e}");
ExitCode::FAILURE
}
}
}

View file

@ -0,0 +1,10 @@
[package]
name = "redox-wl-test-shm-pipeline"
version = "0.1.0"
edition = "2021"
[dependencies]
wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false }
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
libc = "0.2"

View file

@ -0,0 +1,589 @@
//! Test 6 — Pipeline complet wayland-rs sur Redox.
//!
//! C'est ce que fait un compositor Wayland minimal :
//! - serveur expose un socket Unix (ListeningSocket)
//! - client se connecte via WAYLAND_SOCKET (vrai pattern Wayland)
//! - client crée wl_shm_pool depuis un fd shm
//! - client crée wl_buffer depuis le pool, peint un pattern ARGB
//! - client crée wl_surface, attach, damage, commit
//! - serveur reçoit le commit, mappe le buffer fd, lit les pixels, vérifie
//!
//! Si ce test passe, le pipeline shm Wayland complet fonctionne sur Redox.
//! C'est la preuve technique que la phase 4-7 du plan directeur (display
//! backend + input + surfaces shm + compositor minimal) est réalisable
//! sans surprise majeure.
use std::ffi::CString;
use std::io;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::process::ExitCode;
use std::ptr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use wayland_client::{
Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue, Proxy as _,
QueueHandle as ClientQueueHandle,
backend::Backend as ClientBackend,
protocol::{
wl_buffer::WlBuffer as ClientBuffer, wl_compositor::WlCompositor as ClientCompositor,
wl_registry, wl_shm::WlShm as ClientShm, wl_shm_pool::WlShmPool as ClientShmPool,
wl_surface::WlSurface as ClientSurface,
},
};
use wayland_server::{
Client, DataInit, Display as ServerDisplay, DisplayHandle, GlobalDispatch,
backend::ClientData,
protocol::{
wl_buffer, wl_callback, wl_compositor, wl_shm, wl_shm_pool, wl_surface,
},
};
const W: i32 = 100;
const H: i32 = 100;
const STRIDE: i32 = W * 4;
const POOL_SIZE: i32 = STRIDE * H;
const SOCKET_NAME: &str = "wayland-test-06";
fn expected_pixel(x: i32, y: i32) -> u32 {
let a: u32 = 0xFF;
let r: u32 = (x as u32) & 0xFF;
let g: u32 = (y as u32) & 0xFF;
let b: u32 = ((x + y) as u32) & 0xFF;
(a << 24) | (r << 16) | (g << 8) | b
}
fn errno_str() -> String {
let e = io::Error::last_os_error();
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
}
// =====================================================================
// Server side
// =====================================================================
#[derive(Default)]
struct ServerObservation {
surface_created: bool,
pool_created_with_fd: Option<i32>,
buffer_created: Option<(i32, i32)>, // (w, h)
surface_attached: bool,
surface_committed: bool,
/// Set by main loop after reading pixels successfully
pixels_verified: bool,
}
struct ServerState {
obs: Arc<Mutex<ServerObservation>>,
}
#[derive(Debug)]
struct DumbClientData;
impl ClientData for DumbClientData {}
// ---- wl_compositor ----
impl GlobalDispatch<wl_compositor::WlCompositor, ()> for ServerState {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: wayland_server::New<wl_compositor::WlCompositor>,
_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
data_init.init(resource, ());
}
}
impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for ServerState {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_compositor::WlCompositor,
request: wl_compositor::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if let wl_compositor::Request::CreateSurface { id } = request {
data_init.init(id, ());
state.obs.lock().unwrap().surface_created = true;
eprintln!("[server] wl_compositor.create_surface");
}
}
}
// ---- wl_shm ----
impl GlobalDispatch<wl_shm::WlShm, ()> for ServerState {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: wayland_server::New<wl_shm::WlShm>,
_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
let shm = data_init.init(resource, ());
// Advertise the format
shm.format(wl_shm::Format::Argb8888);
}
}
impl wayland_server::Dispatch<wl_shm::WlShm, ()> for ServerState {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_shm::WlShm,
request: wl_shm::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if let wl_shm::Request::CreatePool { id, fd, size } = request {
let raw_fd = fd.as_raw_fd();
// Dup the fd so we keep it after the OwnedFd in the request is dropped
let our_fd = unsafe { libc::dup(raw_fd) };
state.obs.lock().unwrap().pool_created_with_fd = Some(our_fd);
data_init.init(id, PoolData { fd: our_fd, size });
eprintln!("[server] wl_shm.create_pool fd={raw_fd} size={size} (server dup={our_fd})");
}
}
}
// ---- wl_shm_pool ----
struct PoolData {
fd: i32,
size: i32,
}
impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, PoolData> for ServerState {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_shm_pool::WlShmPool,
request: wl_shm_pool::Request,
_data: &PoolData,
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if let wl_shm_pool::Request::CreateBuffer {
id,
offset,
width,
height,
stride,
format,
} = request
{
data_init.init(id, ());
state.obs.lock().unwrap().buffer_created = Some((width, height));
eprintln!(
"[server] wl_shm_pool.create_buffer offset={offset} {width}x{height} stride={stride} format={format:?}"
);
}
}
}
// ---- wl_buffer ----
impl wayland_server::Dispatch<wl_buffer::WlBuffer, ()> for ServerState {
fn request(
_state: &mut Self,
_client: &Client,
_resource: &wl_buffer::WlBuffer,
_request: wl_buffer::Request,
_data: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
// ---- wl_surface ----
impl wayland_server::Dispatch<wl_surface::WlSurface, ()> for ServerState {
fn request(
state: &mut Self,
_client: &Client,
_resource: &wl_surface::WlSurface,
request: wl_surface::Request,
_data: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
match request {
wl_surface::Request::Attach { .. } => {
state.obs.lock().unwrap().surface_attached = true;
eprintln!("[server] wl_surface.attach");
}
wl_surface::Request::Damage { .. }
| wl_surface::Request::DamageBuffer { .. } => {
eprintln!("[server] wl_surface.damage");
}
wl_surface::Request::Commit => {
state.obs.lock().unwrap().surface_committed = true;
eprintln!("[server] wl_surface.commit");
}
_ => {}
}
}
}
impl wayland_server::Dispatch<wl_callback::WlCallback, ()> for ServerState {
fn request(
_state: &mut Self,
_client: &Client,
_resource: &wl_callback::WlCallback,
_request: wl_callback::Request,
_data: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
// =====================================================================
// Client side
// =====================================================================
#[derive(Default)]
struct ClientState {
compositor: Option<ClientCompositor>,
shm: Option<ClientShm>,
}
impl ClientDispatch<wl_registry::WlRegistry, ()> for ClientState {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_data: &(),
_conn: &ClientConnection,
qh: &ClientQueueHandle<Self>,
) {
if let wl_registry::Event::Global { name, interface, version } = event {
eprintln!("[client] global #{name}: {interface} v{version}");
match interface.as_str() {
"wl_compositor" => {
state.compositor = Some(registry.bind(name, version, qh, ()));
}
"wl_shm" => {
state.shm = Some(registry.bind(name, version, qh, ()));
}
_ => {}
}
}
}
}
macro_rules! noop_dispatch {
($iface:ty) => {
impl ClientDispatch<$iface, ()> for ClientState {
fn event(
_state: &mut Self,
_r: &$iface,
_event: <$iface as Proxy>::Event,
_data: &(),
_conn: &ClientConnection,
_qh: &ClientQueueHandle<Self>,
) {
}
}
};
}
use wayland_client::Proxy;
noop_dispatch!(ClientCompositor);
noop_dispatch!(ClientShm);
noop_dispatch!(ClientShmPool);
noop_dispatch!(ClientBuffer);
noop_dispatch!(ClientSurface);
// =====================================================================
// Process orchestration
// =====================================================================
unsafe fn create_shm_buffer(name: &str, size: usize) -> Result<(OwnedFd, *mut u8), String> {
let cname = CString::new(name).unwrap();
let _ = libc::shm_unlink(cname.as_ptr());
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600);
if fd < 0 {
return Err(format!("shm_open: {}", errno_str()));
}
if libc::ftruncate(fd, size as _) != 0 {
libc::close(fd);
return Err(format!("ftruncate: {}", errno_str()));
}
let p = libc::mmap(
ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
0,
);
if p == libc::MAP_FAILED {
libc::close(fd);
return Err(format!("mmap: {}", errno_str()));
}
Ok((unsafe { OwnedFd::from_raw_fd(fd) }, p as *mut u8))
}
use std::os::fd::FromRawFd;
unsafe fn child_main(server_path: &str) -> ! {
eprintln!("[client pid={}] connecting to {server_path}", libc::getpid());
// Connect to the server socket
let stream = match UnixStream::connect(server_path) {
Ok(s) => s,
Err(e) => {
eprintln!("[client] connect: {e}");
libc::_exit(2);
}
};
let fd = stream.into_raw_fd();
// wayland-client connect_to_env reads WAYLAND_SOCKET
std::env::set_var("WAYLAND_SOCKET", fd.to_string());
let conn = match ClientConnection::connect_to_env() {
Ok(c) => c,
Err(e) => {
eprintln!("[client] connect_to_env: {e}");
libc::_exit(3);
}
};
let mut event_queue: EventQueue<ClientState> = conn.new_event_queue();
let qh = event_queue.handle();
let _registry = conn.display().get_registry(&qh, ());
let mut state = ClientState::default();
// Roundtrip to receive globals
if let Err(e) = event_queue.roundtrip(&mut state) {
eprintln!("[client] roundtrip 1: {e}");
libc::_exit(4);
}
if state.compositor.is_none() || state.shm.is_none() {
eprintln!("[client] missing compositor/shm globals");
libc::_exit(5);
}
eprintln!("[client] got compositor + shm globals");
// Create the shm buffer
let (shm_fd, map) = match create_shm_buffer("/wl-test-06-buffer", POOL_SIZE as usize) {
Ok(v) => v,
Err(e) => {
eprintln!("[client] shm: {e}");
libc::_exit(6);
}
};
// Paint pattern
let pixels = std::slice::from_raw_parts_mut(map as *mut u32, (W * H) as usize);
for y in 0..H {
for x in 0..W {
pixels[(y * W + x) as usize] = expected_pixel(x, y);
}
}
eprintln!("[client] painted {}x{} ARGB pattern in shm", W, H);
// Create wl_shm_pool from the fd
let shm = state.shm.clone().unwrap();
let pool = shm.create_pool(shm_fd.as_fd(), POOL_SIZE, &qh, ());
// Create wl_buffer
let buffer = pool.create_buffer(
0, W, H, STRIDE,
wayland_client::protocol::wl_shm::Format::Argb8888,
&qh, (),
);
// Create wl_surface
let compositor = state.compositor.clone().unwrap();
let surface = compositor.create_surface(&qh, ());
// attach, damage, commit
surface.attach(Some(&buffer), 0, 0);
surface.damage_buffer(0, 0, W, H);
surface.commit();
eprintln!("[client] attached + damaged + committed");
// Flush all pending writes
if let Err(e) = event_queue.flush() {
eprintln!("[client] flush: {e}");
libc::_exit(7);
}
// Roundtrip to ensure server has processed everything
let _ = event_queue.roundtrip(&mut state);
eprintln!("[client] done, exit 0");
libc::_exit(0);
}
fn run() -> Result<(), String> {
println!("[test-06] Wayland shm pipeline on Redox");
// Pick a socket path. We won't use bind_auto because XDG_RUNTIME_DIR
// is not guaranteed inside the redoxer mini-VM.
let socket_path = format!("/tmp/{SOCKET_NAME}.sock");
let _ = std::fs::remove_file(&socket_path);
let _ = std::fs::remove_file(format!("/tmp/{SOCKET_NAME}.sock.lock"));
let listener = UnixListener::bind(&socket_path)
.map_err(|e| format!("UnixListener::bind({socket_path}): {e}"))?;
listener
.set_nonblocking(true)
.map_err(|e| format!("set_nonblocking: {e}"))?;
println!("[server] listening on {socket_path}");
let obs = Arc::new(Mutex::new(ServerObservation::default()));
let mut display: ServerDisplay<ServerState> = ServerDisplay::new()
.map_err(|e| format!("Display::new: {e:?}"))?;
let mut dh = display.handle();
dh.create_global::<ServerState, wl_compositor::WlCompositor, _>(5, ());
dh.create_global::<ServerState, wl_shm::WlShm, _>(1, ());
// Fork the client
let pid = unsafe { libc::fork() };
if pid < 0 {
return Err(format!("fork: {}", errno_str()));
}
if pid == 0 {
unsafe { child_main(&socket_path) };
}
let mut state = ServerState { obs: obs.clone() };
let start = Instant::now();
let timeout = Duration::from_secs(5);
let mut accepted_pid: Option<libc::pid_t> = None;
while start.elapsed() < timeout {
// Accept new client
if accepted_pid.is_none() {
match listener.accept() {
Ok((stream, _addr)) => {
stream.set_nonblocking(true).ok();
dh.insert_client(stream, Arc::new(DumbClientData))
.map_err(|e| format!("insert_client: {e:?}"))?;
accepted_pid = Some(pid);
println!("[server] accepted client connection");
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
Err(e) => return Err(format!("accept: {e}")),
}
}
display
.dispatch_clients(&mut state)
.map_err(|e| format!("dispatch_clients: {e:?}"))?;
display
.flush_clients()
.map_err(|e| format!("flush_clients: {e:?}"))?;
let snap = obs.lock().unwrap();
if snap.surface_committed && snap.pool_created_with_fd.is_some() {
// We have all we need to verify pixels
break;
}
drop(snap);
std::thread::sleep(Duration::from_millis(10));
}
// Capture observation state
let surface_committed;
let buffer_created;
let surface_attached;
let pool_fd;
{
let s = obs.lock().unwrap();
surface_committed = s.surface_committed;
buffer_created = s.buffer_created;
surface_attached = s.surface_attached;
pool_fd = s.pool_created_with_fd;
}
if !surface_committed {
return Err("server did not observe wl_surface.commit within 5s".into());
}
if buffer_created.is_none() {
return Err("server did not observe wl_shm_pool.create_buffer".into());
}
if !surface_attached {
return Err("server did not observe wl_surface.attach".into());
}
let pool_fd = pool_fd.ok_or("server did not receive shm fd")?;
println!("[server] all expected protocol events observed; mapping fd to verify pixels");
// Map the received fd and verify pixels
let map = unsafe {
libc::mmap(
ptr::null_mut(),
POOL_SIZE as usize,
libc::PROT_READ,
libc::MAP_SHARED,
pool_fd,
0,
)
};
if map == libc::MAP_FAILED {
return Err(format!("server mmap: {}", errno_str()));
}
let pixels = unsafe { std::slice::from_raw_parts(map as *const u32, (W * H) as usize) };
let mut mismatches: Vec<(i32, i32, u32, u32)> = Vec::new();
for y in 0..H {
for x in 0..W {
let got = pixels[(y * W + x) as usize];
let want = expected_pixel(x, y);
if got != want && mismatches.len() < 5 {
mismatches.push((x, y, got, want));
}
}
}
unsafe { libc::munmap(map, POOL_SIZE as usize) };
if !mismatches.is_empty() {
for (x, y, got, want) in &mismatches {
eprintln!("[server] mismatch at ({x},{y}): got {got:#010x} want {want:#010x}");
}
return Err(format!("{} pixel mismatches in {} total", mismatches.len(), W * H));
}
println!(
"[server] all {} pixels match expected pattern (read via fd received through Wayland protocol)",
W * H
);
// Reap child
let mut status: libc::c_int = 0;
unsafe { libc::waitpid(pid, &mut status, 0) };
let exit_code = unsafe { libc::WEXITSTATUS(status) };
if exit_code != 0 {
return Err(format!("child exited with status {exit_code}"));
}
println!("[server] child reaped cleanly");
// Cleanup
let _ = std::fs::remove_file(&socket_path);
let _ = std::fs::remove_file(format!("{socket_path}.lock"));
let cname = CString::new("/wl-test-06-buffer").unwrap();
unsafe { libc::shm_unlink(cname.as_ptr()) };
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => {
println!("[test-06] PASS: full Wayland shm pipeline works on Redox");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("[test-06] FAIL: {e}");
ExitCode::FAILURE
}
}
}

View file

@ -0,0 +1,7 @@
[package]
name = "redox-wl-test-unix-socket"
version = "0.1.0"
edition = "2021"
[dependencies]
libc = "0.2"

View file

@ -0,0 +1,78 @@
//! Test 1 — Unix socket SOCK_STREAM with Wayland-shaped messages.
//!
//! Wayland wire format (libwayland reference):
//! - Header: object_id (u32) + (size << 16 | opcode) (u32) = 8 bytes
//! - Payload: variable, 4-byte aligned
//!
//! This test:
//! 1. Creates a SOCK_STREAM socketpair (AF_UNIX)
//! 2. Side A sends a message with header + payload
//! 3. Side B reads it back, validates byte-for-byte
//! 4. Ensures partial reads don't corrupt the stream
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::process::ExitCode;
const OBJECT_ID: u32 = 0x0000_0001;
const OPCODE: u16 = 0x000A;
const PAYLOAD: &[u8] = b"hello-wayland-on-redox-roundtrip";
fn main() -> ExitCode {
println!("[test-01] Unix socket SOCK_STREAM Wayland-shaped roundtrip");
let (mut a, mut b) = match UnixStream::pair() {
Ok(pair) => pair,
Err(e) => {
eprintln!("[test-01] FAIL: UnixStream::pair: {e}");
return ExitCode::FAILURE;
}
};
let payload_len = PAYLOAD.len();
let aligned_len = (payload_len + 3) & !3;
let total_size = 8 + aligned_len;
let mut msg = Vec::with_capacity(total_size);
msg.extend_from_slice(&OBJECT_ID.to_ne_bytes());
let size_op: u32 = ((total_size as u32) << 16) | (OPCODE as u32);
msg.extend_from_slice(&size_op.to_ne_bytes());
msg.extend_from_slice(PAYLOAD);
msg.resize(total_size, 0);
if let Err(e) = a.write_all(&msg) {
eprintln!("[test-01] FAIL: write_all: {e}");
return ExitCode::FAILURE;
}
println!("[test-01] sent {} bytes (header 8 + payload {} aligned to {})",
total_size, payload_len, aligned_len);
let mut header = [0u8; 8];
if let Err(e) = b.read_exact(&mut header) {
eprintln!("[test-01] FAIL: read header: {e}");
return ExitCode::FAILURE;
}
let recv_oid = u32::from_ne_bytes(header[0..4].try_into().unwrap());
let recv_so = u32::from_ne_bytes(header[4..8].try_into().unwrap());
let recv_size = (recv_so >> 16) as usize;
let recv_op = (recv_so & 0xFFFF) as u16;
if recv_oid != OBJECT_ID || recv_op != OPCODE || recv_size != total_size {
eprintln!("[test-01] FAIL: header mismatch oid={recv_oid:#x} op={recv_op:#x} size={recv_size}");
return ExitCode::FAILURE;
}
let mut buf = vec![0u8; aligned_len];
if let Err(e) = b.read_exact(&mut buf) {
eprintln!("[test-01] FAIL: read payload: {e}");
return ExitCode::FAILURE;
}
if &buf[..payload_len] != PAYLOAD {
eprintln!("[test-01] FAIL: payload mismatch");
return ExitCode::FAILURE;
}
println!("[test-01] PASS: roundtrip OK, {} bytes recv, oid={:#x} op={:#x}",
total_size, recv_oid, recv_op);
ExitCode::SUCCESS
}