惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
S
Security @ Cisco Blogs
博客园 - 三生石上(FineUI控件)
博客园 - 叶小钗
Last Week in AI
Last Week in AI
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
GbyAI
GbyAI
Microsoft Security Blog
Microsoft Security Blog
B
Blog
A
About on SuperTechFans
B
Blog RSS Feed
M
MIT News - Artificial intelligence
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
宝玉的分享
宝玉的分享
H
Hackread – Cybersecurity News, Data Breaches, AI and More
腾讯CDC
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Help Net Security
博客园 - Franky
D
Darknet – Hacking Tools, Hacker News & Cyber Security
P
Palo Alto Networks Blog
罗磊的独立博客
S
Securelist
L
LINUX DO - 热门话题
T
Tor Project blog
人人都是产品经理
人人都是产品经理
Google DeepMind News
Google DeepMind News
T
Threatpost
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy International News Feed
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Spread Privacy
Spread Privacy
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
爱范儿
爱范儿
Cyberwarzone
Cyberwarzone
NISL@THU
NISL@THU
Apple Machine Learning Research
Apple Machine Learning Research
S
SegmentFault 最新的问题
C
Cyber Attacks, Cyber Crime and Cyber Security
G
Google Developers Blog
The Hacker News
The Hacker News
Latest news
Latest news
A
Arctic Wolf
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
Schneier on Security

Lobsters

CIFSwitch: a non-universal Linux local root vulnerability RIPE NCC session fixation: poaching logins with an Atlas probe GNOME 2.20 but its Web Components Agentic Search for Context Engineering – Leonie Monigatti Garnix is shutting down [not OC] akashina.tngl.sh/jjc Concerning Emacs (and Jazz) Nitpicking the shell history scene in ‘Tron: Legacy’ What's cooking on SourceHut? Q2 2026 The tenth OpenPGP email summit Package managers that package package managers Clojure on Fennel part three: parsing WordPress at 23 Finding Miscompiles for Fun, Not Profit GitHub - creusot-rs/creusot: Creusot helps you prove your Rust code is correct. Announcing Rust 1.96.0 | Rust Blog A Love Letter to Neovim sqlite AGENTS.md Am I a Bad Friend? CSS vs. JavaScript • Josh W. Comeau Erlang Ecosystem Foundation - Supporting the BEAM community A brief note about slot access cost in Common Lisp Keyboard latency probe Rethinking the GNOME clipboard issues Back to the Building Blocks’ Building Blocks Tech Notes: Theseus: translating win32 to wasm Fast is better than slow Content-addressed Rust builds (or, what kache actually caches) Intent to Prototype: Embedding API Canada’s Bill C-22 and the security cost of collecting more data 5 PostgreSQL locking behaviors that trip people up okmij.org Stop advertising in your commits! | AksDev GitHub - mplsllc/macsurf: A modern web browser for Classic Mac OS 9 PowerPC. Real CSS3, ES5 JavaScript, native HTTPS — built with CodeWarrior on the Carbon API. Introducing DoomBench - Can Your Data Stack Run DOOM? What are some of your favourite developer tools? Building a Scalable Ingestion Pipeline with Temporal (Part 1) Converting shallow Git bundles into normal repositories Are you a member of any professional associations? What is a harmonic? An interactive comic about additive synthesis How Virtual Tables Work in the Itanium C++ ABI Using SwiftUI to Build a Mac-assed App in 2026 Rust (and Slint) on a jailbroken Kindle. ~jack/lambda-on-lambda - Serverless Haskell on AWS - sourcehut git Human proof for FOSS contributions Extremely simple internet radio controlled via IRC Announcing BABLR Splitting Konsole views from Helix to run tools | AksDev GitHub - yugr/rust-slides Serving files over HTTP three ways: synchronous, epoll, and io_uring update docs with information about building with build.py (#979) · astral-sh/python-build-standalone@c9c40c5 A Simple Makefile Tutorial On C extensions, portability, and alternative compilers Switching to Colemak | Pedro Alves Just How Bad Was The Intel IAPX432? Nix's Substituter List Is Not a Routing Table Accelerating copy_if using SIMD Lambda on Lambda: Serverless Haskell on AWS | Blog Announcing feed-repeat v1.0 Scaling Akvorado BMP RIB with sharding EYG news: A host of CLI improvements, new guides and new effects The social contract of writing JS Crossword C array types are weird; and related topics Flatpak will depend on systemd – OSnews Migrating from Go to Rust | corrode Rust Consulting A portentous reunion Vivado Licensing Options How my minimal, memory-safe Go rsync steers clear of vulnerabilities the entropy layer of a wavelet codec, on its own GitHub - nferhat/fht-compositor: A dynamic tiling Wayland compositor. Debian SE Linux and PinTheft Does bulk memmove speed up std::remove_if? (No.) 声明式部分更新 | Blog | Chrome for Developers Fully in-browser container builds Dianne Skoll's Web Site - Remind The Architecture of Open Source Applications (Volume 1)Berkeley DB Pardon MIE? - ironPeak Blog “Long-Term Support” doesn’t mean what you think Jira IS Turing-Complete May I recommend thinking of Emacs as your Fortress of Solitude hershey Floodgap Gopher-HTTP gateway gopher://thelambdalab.xyz/1cuneiforth/ HP QuickWeb, Singular And Pointless That one time I used Go panics for flow control A new suite of modern tools coming for editing and publishing RFCs From the Tabletop… The Digital Antiquarian Building a Host-Tuned GCC to Make GCC Compile Faster Are we self-sovereign PKI yet? Claw Patrol: an open-source security firewall for agents | Deno Revised^7 Report on Scheme, Large: Procedural Fascicle Draft is now public A Network Allow-List Won't Stop Exfiltration — André Graf From AFSK to Goertzel – µArt.cz Software For My New Home Server Introducing Neptune: Direct3D virtualization for QEMU AI Agent Bankrupted Their Operator While Trying to Scan DN42 - Lan Tian @ Blog mimalloc: A new, high-performance, scalable memory allocator for the modern era Making wl_shm fast The Soul of Maintaining a New Machine - Third Draft | Books in Progress What is Git made of?
Building an AsyncIO executor for the 3DS (pt 2!)
blog.cat-gir · 2026-05-26 · via Lobsters

in the first part, we learned how to build a minimum executor to drive some futures. now let's do that on a 3DS!

the 3ds operating system

the 3ds runs a microkernel-based operating system called Horizon. drivers for peripherals are just slightly special processes: to interface with peripherals, we use Inter-Process Communication (IPC) to send these processes messages.

additionally, the 3ds provides a series of synchronization objects: things that a thread can wait for.

these two things are the basic pieces we need to build an asyncio executor!

so here we go:

components!

the executor itself

we're making a very minimal executor: its job is only to spawn futures and then get told to poll them.

initially, i used IPC for this - including sending rust future objects through IPC, which was a fun challenge - but this has a big problem: sending an IPC request to the same thread that's currently running breaks things very badly. considering futures can ideally wake each other up, this made lockups frequent.

instead, our executor will have three pieces:

  • a queue for requests to spawn new tasks
  • a queue to wake up existing tasks
  • a loop that waits for items to be added to either queue

here's our queues:

pub static TASK_QUEUE: OnceLock<SyncQueue<BoxFuture<'static, ()>>> = OnceLock::new();
pub static WAKE_QUEUE: OnceLock<SyncQueue<TaskToken>> = OnceLock::new();

these use the 3ds built-in semaphore primitive, to let other tasks signal us to wake up! we can wait on several semaphores at once and get woken up when any one of them is signalled:

let handles = [self.task_semaphore, self.wake_semaphore];
loop {
    let mut idx = 0;
    unsafe { svcWaitSynchronizationN(&mut idx, handles.as_ptr(), 2, false, i64::MAX) };
    if idx == 1 { // this is the wake semaphore!
        while let Some(task) = WAKE_QUEUE.get().unwrap().remove() {
            self.wake(task);
        }
    } else if idx == 0 { // this is the spawn semaphore!
        while let Some(task) = TASK_QUEUE.get().unwrap().remove() {
            self.spawn(task);
        }
    }
}
an aside: wakers

in our previous post, our Waker that we passed to futures was an Arc'd object containing both an id for the current task and a sender channel that linked up to the executor. we can trim this down a lot now!

to make a non-Arc'd waker, we can use the RawWaker type, which is constructed from (a) a pointer to some data, and (b) a set of pointers to functions that take that data as an argument to wake a task.

usually, the pointer is just to an Arc or a similar type. but pointers are just integers, and we're using integers to ID our tasks anyway... so we can just pass our task id as the "data pointer" and cast it back to a u32!

// snip: <impls for clone_waker, wake_by_ref, drop>

pub struct TaskToken(pub u32);

unsafe fn wake(data: *const ()) {
    WAKE_QUEUE.get().unwrap().add(TaskToken(data as u32));
}

static VTABLE: RawWakerVTable = RawWakerVTable::new(clone_waker, wake, wake_by_ref, drop);

pub fn make_waker(task: TaskToken) -> Waker {
    unsafe { Waker::from_raw(RawWaker::new(task.0 as *const (), &VTABLE)) }
}

sleeping & timers

we have an executor now! let's make it do something useful, like sleeping.

to implement this, we want to run a thread that:

  • receives requests to set up new timers for tasks,
  • waits for existing timers to complete, and wakes the tasks that asked for them

to run both concurrently, we can do a fun trick. remember how we're using the 3ds' Inter-Process-Communication (IPC) system instead of standard channels?

it turns out, the same system call that listens for IPC requests can also listen for.. anything that can be waited on in the 3ds' operating system? events, mutexes, timers: it can do it all.

implementing it (with some healthy abstraction)
struct ReactorHandler {
    wait_tokens: LiteMap<OSHandle, Vec<TaskToken>>,
}

impl IPCServerHandler<ReactorCmd, ReactorReply> for ReactorHandler {
    // handles IPC requests
    fn handle_request(
        &mut self,
        request: ReactorCmd,
        server: &mut IPCServer<ReactorCmd, ReactorReply>,
    ) -> ReactorReply {
        match request {
            ReactorCmd::AddHandle(task, handle) => {
                self.wait_tokens.entry(handle).or_default().push(task);
                server.add_handle_to_list(handle);
                ReactorReply::Ok
            }
            ReactorCmd::PopHandle => todo!(),
        }
    }

    // handles non-IPC (any other synchronization object)
    fn handle_additional_oshandle(
        &mut self,
        handle: OSHandle,
        _server: &mut IPCServer<ReactorCmd, ReactorReply>,
    ) {
        for token in self.wait_tokens.remove(&handle).unwrap() {
            WAKE_QUEUE.get().unwrap().add(token);
        }
    }
}

input, output!

files and sockets is where it gets a little bit more complicated. operations on both are implemented as IPC calls, and these are always synchronous: we can't convert an ongoing operation into a synchronization object that we wait on concurrently with other things; it's more akin to a normal function call.

instead, we use a thread pool: file operations will force a thread to yield if they aren't ready, so we can use them without hogging CPU.

so to do a file operation, we submit it to a thread, alongside the id for our task. the worker then picks it up, and uses the id for our task to wake us up when the operation is done!

and let's implement it

more specifically, we send a message containing the file's operating-system handle (a file descriptor, for unix people), the id of our task, a couple of pointers where the worker will store the result of the operation, and a pointer to the buffer where the actual data will be stored.

#[derive(IPCMessage)]
#[repr(u32)]
pub(crate) enum AsyncFsMsg {
    Write(#[flatten] FileIoOperation) = 0xA,
    Read(#[flatten] FileIoOperation) = 0xB,
    Flush(#[flatten] FileControlOperation) = 0xC,
    Close(#[flatten] FileControlOperation) = 0xD,
}

#[derive(IPCSerializable)]
pub(crate) struct FileIoOperation {
    #[normal]
    pub file: u32,
    #[normal]
    pub task: TaskToken,
    #[normal]
    pub state: u32, // atomici32 ptr (result code),
    #[normal]
    pub result: u32, // atomicu32 ptr (u32::MAX if not done, else bytes read)
    #[normal]
    pub offset: u32,
    #[normal]
    pub len: u32,
    #[normal]
    pub data_ptr: u32,
}

impl FileIoOperation {
    fn view<'a>(&'a self) -> FileIoOperationView<'a> {
        FileIoOperationView {
            file: ManuallyDrop::new(unsafe { FileHandle::from_raw(self.file) }),
            task: self.task,
            state: unsafe { AtomicI32::from_ptr(self.state as *mut i32) },
            result: unsafe { AtomicU32::from_ptr(self.result as *mut u32) },
            offset: self.offset,
            data: unsafe {
                std::slice::from_raw_parts(self.data_ptr as *const u8, self.len as usize)
            },
        }
    }
}

on the worker side:

match task {
    AsyncFsMsg::Write(op) => {
        info!("io.worker.{} write fd:{:x}", self.id, op.file);

        let mut op = op.view();
        let res: BunnyResult<usize> =
            op.file
                .write(op.offset as u64, op.data, super::WriteOptions::empty());
        op.resolve(res);
        self.executor.wake(op.task).unwrap();
    },
    _ => ...
}

and our future:

impl<'a> Future for Write<'a> {
    type Output = BunnyResult<usize>;

    fn poll(
        mut self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Self::Output> {
        let bytes_read = self.bytes_written.load(Ordering::Acquire);
        if bytes_read == u32::MAX && !self.registered {
            if let Err(e) = self.client.request(&AsyncFsMsg::Write(FileIoOperation {
                file: self.file.inner.session,
                task: TaskToken::from_waker(cx.waker()),
                state: self.state.as_ptr() as u32,
                result: self.bytes_written.as_ptr() as u32,
                offset: self.offset,
                len: self.data.len() as u32,
                data_ptr: self.data.as_ptr() as u32,
            })) {
                Poll::Ready(Err(e.into()))
            } else {
                self.registered = true;
                Poll::Pending
            }
        } else if self.registered && bytes_read != u32::MAX {
            state_resolve(&self.state, bytes_read as usize)
        } else {
            Poll::Pending
        }
    }
}

locks

if we wanted to implement an async-only lock or mutex, that's easy: we just keep a list of tasks that are waiting on the lock. to lock it, we add ourselves to the list; to unlock it, we wake up the next task up on the list if there is one.

making a lock that can interface both synchronous and asynchronous code is a little bit trickier. my approach has been to modify libctru's LightLock, which uses a lightweight synchronization primitive called an AddressArbitrator to signal threads.

the full code is a little bit bulky to put on here, but here's the highlights. if you want to see the full impl, it lives here

impl DsLock {
    // if we want to use this from a non-async contest, we use TaskToken(u32::MAX) to mark that
    fn try_lock_and_register(&self, token: TaskToken) -> bool {
        let state = self.arb.0.load(Ordering::Acquire);
        if state > 0 {
            self.arb.0.store(-state, Ordering::Release);
            true
        } else {
            self.arb.0.store(state - 1, Ordering::Release);
            self.waiters.lock().push(token);
            false
        }
    }
    
    unsafe fn unlock(&self) {
        let lock_state = self.arb.0.load(Ordering::Acquire);
        if lock_state < 0 {
            self.arb.0.store(-lock_state, Ordering::Release);
            let mut waiters = self.waiters.lock();
            if waiters.is_empty() {
                return;
            }

            // get the next task to wake up...
            let task = waiters.remove(0);
            drop(waiters);

            if task == TaskToken(u32::MAX) {
                // it's a non-async one, signal it using the AddressArbitrator
                let _ = self.arb.signal_one();
            } else {
                // it's an async task, wake it up using the executor
                executor::WAKE_QUEUE.get().unwrap().add(task);
            }
        }
    }
}

putting it all together

here's a tiny example, using egui_citro3d (originally by github.com/LexiBigCheese, with some modifications by me).

it combines an asynchronous TCP socket with a blocking UI task, to display messages sent on the screen!

// full example: https://github.com/kore-signet/bunnyds-egui-example

let server = AsyncTcpSocket::bind("0.0.0.0:3027")?;
let client = server.accept().await?;

let messages: Arc<DSMutex<Vec<String>>> = Arc::new(DSMutex::new(Vec::new()));
let messages_tx: Arc<DSMutex<Vec<String>>> = Arc::clone(&messages);

let message_task = bunnyds::spawn(async move {
    let mut rd_buf = [0u8; 1024];
    while let Ok(bytes_read) = client.recv(&mut rd_buf).await {
        if bytes_read == 0 {
            return;
        }

        messages_tx
            .lock()
            .await
            .push(String::from_utf8_lossy(&rd_buf[..bytes_read]).into());
    }
});

let ui_task = bunnyds::spawn_blocking(Some(0x18), Some(2), move || {
    let mut hid = Hid::new().unwrap();
    let apt = Apt::new().unwrap();
    let gfx = Gfx::new().unwrap();
    let mut egui_ctx = EguiRenderer::new(&mut hid, &gfx, &apt, &key_mapping);
    while apt.main_loop() {
        gfx.wait_for_vblank();
        egui_ctx.render_frame(
            |ui| {
                egui::CentralPanel::default().show_inside(ui, |ui| {
                    let messages = messages.lock_sync();
                    for message in messages.iter() {
                        ui.label(message.trim());
                    }
                });
            },
            |_ui| {},
        );
    }
});

futures::future::join(ui_task, message_task).await;

this has been a very fun little adventure! if you have questions about this post or writing rust code on the 3ds, feel free to email me at kore at cat-girl.gay.

you can find the full code for the project at github.com/kore-signet/bunnyds.

an additional side-project of this whole thing has been making a library to abstract the 3ds IPC system into rust primitives, which lives here. if you think an article on that would be fun, let me know!

this whole project would not have been possible without: