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.
[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"]
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,
}
}
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.