AphrodyBun GitHub

Runtime docs · Guides · Runtime & Debugging

Open a COSMIC (libcosmic) window with bun:ffi

libcosmic is the Rust toolkit of the COSMIC desktop (Pop!_OS), built on iced and winit. Export a function from a Rust cdylib that runs a cosmic::Application, and call it from bun:ffi. The event loop runs on the calling thread, which is the JavaScript thread.

Cargo.toml
[package]
name = "cosmic-window"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
default-features = false
features = ["winit", "tokio"]

# Wayland and X11 backends exist on Linux only.
[target.'cfg(target_os = "linux")'.dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
default-features = false
features = ["winit", "tokio", "wayland", "x11"]
src/lib.rs
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::Size;
use cosmic::{Element, executor, widget};

struct Window {
    core: Core,
}

impl cosmic::Application for Window {
    type Executor = executor::Default;
    type Flags = ();
    type Message = ();
    const APP_ID: &'static str = "sh.bun.Example";

    fn core(&self) -> &Core {
        &self.core
    }
    fn core_mut(&mut self) -> &mut Core {
        &mut self.core
    }
    fn init(core: Core, _flags: ()) -> (Self, Task<()>) {
        (Window { core }, Task::none())
    }
    fn view(&self) -> Element<'_, ()> {
        widget::text::body("libcosmic through bun:ffi").into()
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn cosmic_window_run(width: u32, height: u32) -> i32 {
    let settings = Settings::default()
        .size(Size::new(width as f32, height as f32))
        .transparent(false);
    match cosmic::app::run::<Window>(settings, ()) {
        Ok(()) => 0,
        Err(_) => 1,
    }
}
window.ts
import { dlopen, FFIType, suffix } from "bun:ffi";

const { symbols } = dlopen(`./target/release/${process.platform === "win32" ? "" : "lib"}cosmic_window.${suffix}`, {
  cosmic_window_run: { args: [FFIType.u32, FFIType.u32], returns: FFIType.i32 },
});
process.exit(symbols.cosmic_window_run(640, 400)); // returns when the window closes
cargo build --release
bun window.ts

Building libcosmic needs pkg-config, cmake and the xkbcommon, Wayland, fontconfig and freetype development headers (on Ubuntu 24.04 and Pop!_OS 24.04: libxkbcommon-dev libwayland-dev libfontconfig-dev libfreetype-dev libexpat1-dev). With default-features = false, iced renders in software with tiny-skia, so the window also opens under xvfb-run in CI. .transparent(false) keeps the window on a 24-bit visual: softbuffer rejects the 32-bit ARGB visual that a transparent window gets under Xvfb.

Callbacks into JavaScript must stay on the thread that called into the library. If you pass a JSCallback to the cdylib, call it only from that thread, for example from Application::init after checking std::thread::current().id().

Tested platforms

The fixture prints window created and window closed with exit code 0 on:

  • Windows 11: winit/Win32 backend, MSVC toolchain
  • Ubuntu 26.04: X11 backend under xvfb-run

The Linux images are scripts/aphrody/gui/ubuntu.Dockerfile and scripts/aphrody/gui/alpine.Dockerfile.


A fuller version with a JSCallback, --timeout (an iced time subscription that returns cosmic::iced::exit()), and --title, --width and --height options lives in the Bun repository at test/js/bun/ffi/cosmic-window.fixture.ts with its crate in test/js/bun/ffi/cosmic-window. To show a libcosmic window without writing Rust, bun:cosmic runs one in a separate helper process.

See Docs > Runtime > FFI for the full bun:ffi reference.