waylite

2 commits
Updated 2026-04-23 15:23:13
examples
examples/plop.rs
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;

const ID_REGISTRY: u32 = 2;
const ID_CALLBACK: u32 = 3;
const ID_COMPOSITOR: u32 = 4;
const ID_WM_BASE: u32 = 5;
const ID_SHM: u32 = 6;
const ID_SURFACE: u32 = 7;
const ID_XDG_SURFACE: u32 = 8;
const ID_TOPLEVEL: u32 = 9;

const ID_SHM_POOL: u32 = 10;
const ID_BUFFER: u32 = 11;

const WIDTH: u32 = 640;
const HEIGHT: u32 = 480;

fn main() {
    let mut stream = UnixStream::connect(PathBuf::new().join("/run/user/1000/wayland-0"))
        .expect("failed to connect");

    // Bootstrap : récupère le registry puis sync
    stream
        .write_all(&msg::encode(1, 1, &[ID_REGISTRY]))
        .unwrap(); // get_registry
    stream
        .write_all(&msg::encode(1, 0, &[ID_CALLBACK]))
        .unwrap(); // sync

    let (mut compositor_name, mut wm_base_name, mut shm_name) = (0u32, 0u32, 0u32);
    pump(&mut stream, |_, object_id, opcode, payload| {
        match (object_id, opcode) {
            (ID_REGISTRY, 0) => {
                println!("{}", payload.len());
                let name = msg::read_u32(payload, 0);
                let (iface, _) = msg::read_string(payload, 4);

                match iface {
                    "wl_compositor" => compositor_name = name,
                    "xdg_wm_base" => wm_base_name = name,
                    "wl_shm" => shm_name = name,
                    _ => {}
                }
                false
            }
            (ID_CALLBACK, 0) => true,
            _ => false,
        }
    });

    println!(
        "wl_compositor name={}, xdg_wm_base name={}, wl_shm={}",
        compositor_name, wm_base_name, shm_name
    );

    // --- Phase 2 : bind wl_compositor et xdg_wm_base ---
    // wl_registry.bind(name, interface, version, new_id)  opcode=0
    stream
        .write_all(&msg::encode_with_str(
            ID_REGISTRY,
            0,
            &[compositor_name],
            "wl_compositor",
            &[4, ID_COMPOSITOR],
        ))
        .unwrap();
    stream
        .write_all(&msg::encode_with_str(
            ID_REGISTRY,
            0,
            &[wm_base_name],
            "xdg_wm_base",
            &[2, ID_WM_BASE],
        ))
        .unwrap();
    stream
        .write_all(&msg::encode_with_str(
            ID_REGISTRY,
            0,
            &[shm_name],
            "wl_shm",
            &[1, ID_SHM],
        ))
        .unwrap();

    // --- Phase 3 : crée surface + xdg_surface + toplevel ---
    stream
        .write_all(&msg::encode(ID_COMPOSITOR, 0, &[ID_SURFACE]))
        .unwrap(); // create_surface
    stream
        .write_all(&msg::encode(ID_WM_BASE, 2, &[ID_XDG_SURFACE, ID_SURFACE]))
        .unwrap(); // get_xdg_surface
    stream
        .write_all(&msg::encode(ID_XDG_SURFACE, 1, &[ID_TOPLEVEL]))
        .unwrap(); // get_toplevel

    // set_title (opcode=2) et set_app_id (opcode=3)
    stream
        .write_all(&msg::encode_with_str(
            ID_TOPLEVEL,
            2,
            &[],
            "Hello Wayland",
            &[],
        ))
        .unwrap();
    stream
        .write_all(&msg::encode_with_str(
            ID_TOPLEVEL,
            3,
            &[],
            "wayland-light",
            &[],
        ))
        .unwrap();

    // Premier commit : demande au compositor de configurer la fenêtre
    stream.write_all(&msg::encode(ID_SURFACE, 6, &[])).unwrap(); // wl_surface.commit

    // --- Phase 4 : attend xdg_surface.configure puis ack ---
    pump(&mut stream, |stream, object_id, opcode, payload| {
        match (object_id, opcode) {
            (ID_WM_BASE, 0) => {
                // xdg_wm_base.ping → pong obligatoire
                let serial = msg::read_u32(payload, 0);
                stream
                    .write_all(&msg::encode(ID_WM_BASE, 3, &[serial]))
                    .unwrap();
                false
            }
            (ID_XDG_SURFACE, 0) => {
                // xdg_surface.configure
                let serial = msg::read_u32(payload, 0);
                stream
                    .write_all(&msg::encode(ID_XDG_SURFACE, 4, &[serial]))
                    .unwrap(); // ack
                stream.write_all(&msg::encode(ID_SURFACE, 6, &[])).unwrap(); // commit
                println!("Window configured! serial={}", serial);
                true // stop
            }
            _ => {
                println!("server id: {object_id}");
                false
            }
        }
    });

    // --- Phase 5 : crée le buffer SHM et dessine dedans ---
    let size = (WIDTH * HEIGHT * 4) as usize;
    let stride = WIDTH * 4;
    let (memfd, pixels) = shm::create(size);

    // Rempli d'un bleu-vert
    unsafe {
        for i in 0..(WIDTH * HEIGHT) as isize {
            *pixels.offset(i) = 0xFF2080A0; // ARGB
        }
    }

    // wl_shm.create_pool(new_id, fd, size) — le fd passe via SCM_RIGHTS
    let create_pool = msg::encode(ID_SHM, 0, &[ID_SHM_POOL, size as u32]);
    shm::send_with_fd(stream.as_raw_fd(), &create_pool, memfd);

    // wl_shm_pool.create_buffer(new_id, offset, width, height, stride, format=XRGB8888=1)
    stream
        .write_all(&msg::encode(
            ID_SHM_POOL,
            0,
            &[ID_BUFFER, 0, WIDTH, HEIGHT, stride, 1],
        ))
        .unwrap();

    // attach + damage + commit
    stream
        .write_all(&msg::encode(ID_SURFACE, 1, &[ID_BUFFER, 0, 0]))
        .unwrap();
    stream
        .write_all(&msg::encode(ID_SURFACE, 2, &[0, 0, WIDTH, HEIGHT]))
        .unwrap();
    stream.write_all(&msg::encode(ID_SURFACE, 6, &[])).unwrap();

    println!("Fenêtre créée. Appuie sur Entrée pour quitter.");
    let _ = std::io::stdin().read_line(&mut String::new());
}

// Pompe les messages jusqu'à ce que le callback retourne true
fn pump<F: FnMut(&mut UnixStream, u32, u16, &[u8]) -> bool>(
    stream: &mut UnixStream,
    mut on_msg: F,
) {
    let mut buf = vec![0u8; 4096];
    let mut pending = Vec::<u8>::new();
    loop {
        let n = stream.read(&mut buf).expect("read");
        pending.extend_from_slice(&buf[..n]);
        let mut offset = 0;
        while let Some((m, consumed)) = msg::decode(&pending[offset..]) {
            let stop = on_msg(stream, m.object_id, m.opcode, m.payload);
            offset += consumed;
            if stop {
                pending.drain(..offset);
                return;
            }
        }
        pending.drain(..offset);
    }
}

mod msg {
    // Encode un message Wayland : [object_id][size<<16|opcode][args...]
    pub fn encode(object_id: u32, opcode: u16, args: &[u32]) -> Vec<u8> {
        let size = (8 + args.len() * 4) as u32;
        let mut buf = Vec::with_capacity(size as usize);
        buf.extend_from_slice(&object_id.to_ne_bytes());
        buf.extend_from_slice(&((size << 16) | opcode as u32).to_ne_bytes());
        for arg in args {
            buf.extend_from_slice(&arg.to_ne_bytes());
        }
        buf
    }

    // Un message décodé
    pub struct Msg<'a> {
        pub object_id: u32,
        pub opcode: u16,
        pub payload: &'a [u8],
    }

    // Décode le prochain message dans buf, retourne (msg, bytes_consommés) ou None
    pub fn decode(buf: &[u8]) -> Option<(Msg<'_>, usize)> {
        if buf.len() < 8 {
            return None;
        }
        let object_id = u32::from_ne_bytes(buf[0..4].try_into().unwrap());
        let word2 = u32::from_ne_bytes(buf[4..8].try_into().unwrap());
        let size = (word2 >> 16) as usize;
        let opcode = (word2 & 0xFFFF) as u16;
        if buf.len() < size {
            return None;
        }
        Some((
            Msg {
                object_id,
                opcode,
                payload: &buf[8..size],
            },
            size,
        ))
    }

    // Lit un u32 dans un payload à l'offset donné
    pub fn read_u32(payload: &[u8], offset: usize) -> u32 {
        u32::from_ne_bytes(payload[offset..offset + 4].try_into().unwrap())
    }

    // Lit une string Wayland (u32 len + bytes + padding) dans un payload
    // Retourne (string, offset_après_padding)
    pub fn read_string(payload: &[u8], offset: usize) -> (&str, usize) {
        let len = read_u32(payload, offset) as usize; // inclut le \0
        let s = std::str::from_utf8(&payload[offset + 4..offset + 4 + len - 1]).unwrap_or("?");
        let next = offset + 4 + len + (4 - len % 4) % 4;
        (s, next)
    }

    // Encode un message dont un argument est une string Wayland
    // before/after = les args u32 autour de la string
    pub fn encode_with_str(
        object_id: u32,
        opcode: u16,
        before: &[u32],
        s: &str,
        after: &[u32],
    ) -> Vec<u8> {
        let slen = s.len() + 1; // +1 pour le \0
        let padding = (4 - slen % 4) % 4;
        let size = 8 + before.len() * 4 + 4 + slen + padding + after.len() * 4;
        let mut buf = Vec::with_capacity(size);
        buf.extend_from_slice(&object_id.to_ne_bytes());
        buf.extend_from_slice(&((size as u32) << 16 | opcode as u32).to_ne_bytes());
        for a in before {
            buf.extend_from_slice(&a.to_ne_bytes());
        }
        buf.extend_from_slice(&(slen as u32).to_ne_bytes());
        buf.extend_from_slice(s.as_bytes());
        buf.push(0);
        for _ in 0..padding {
            buf.push(0);
        }
        for a in after {
            buf.extend_from_slice(&a.to_ne_bytes());
        }
        buf
    }
}

mod shm {
    use std::os::unix::io::RawFd;

    // Crée un memfd de la taille donnée et le mmap en lecture/écriture.
    // Retourne (fd, pointeur vers les pixels).
    pub fn create(size: usize) -> (RawFd, *mut u32) {
        unsafe {
            // memfd_create via syscall direct
            let fd = libc::syscall(libc::SYS_memfd_create, b"shm\0".as_ptr(), 0) as RawFd;
            assert!(fd >= 0, "memfd_create failed");

            let ret = libc::ftruncate(fd, size as i64);
            assert_eq!(ret, 0, "ftruncate failed");

            let ptr = libc::mmap(
                std::ptr::null_mut(),
                size,
                libc::PROT_READ | libc::PROT_WRITE,
                libc::MAP_SHARED,
                fd,
                0,
            );
            assert_ne!(ptr, libc::MAP_FAILED, "mmap failed");

            (fd, ptr as *mut u32)
        }
    }

    // Envoie un message Wayland + un fd via SCM_RIGHTS sur le socket.
    pub fn send_with_fd(socket_fd: RawFd, data: &[u8], fd_to_send: RawFd) {
        unsafe {
            // Ancillary buffer pour SCM_RIGHTS
            let cmsg_space = libc::CMSG_SPACE(std::mem::size_of::<RawFd>() as u32) as usize;
            let mut cmsg_buf = vec![0u8; cmsg_space];

            let mut iov = libc::iovec {
                iov_base: data.as_ptr() as *mut _,
                iov_len: data.len(),
            };
            let mut mhdr: libc::msghdr = std::mem::zeroed();
            mhdr.msg_iov = &mut iov;
            mhdr.msg_iovlen = 1;
            mhdr.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
            mhdr.msg_controllen = cmsg_space as _;

            let cmsg = libc::CMSG_FIRSTHDR(&mhdr);
            (*cmsg).cmsg_level = libc::SOL_SOCKET;
            (*cmsg).cmsg_type = libc::SCM_RIGHTS;
            (*cmsg).cmsg_len = libc::CMSG_LEN(std::mem::size_of::<RawFd>() as u32) as _;
            *(libc::CMSG_DATA(cmsg) as *mut RawFd) = fd_to_send;

            let ret = libc::sendmsg(socket_fd, &mhdr, 0);
            assert!(
                ret >= 0,
                "sendmsg failed: {}",
                std::io::Error::last_os_error()
            );
        }
    }
}