diff --git a/ctru-rs/examples/output-3dslink.rs b/ctru-rs/examples/output-3dslink.rs
new file mode 100644
index 0000000..fd5a526
--- /dev/null
+++ b/ctru-rs/examples/output-3dslink.rs
@@ -0,0 +1,35 @@
+use ctru::gfx::Gfx;
+use ctru::services::apt::Apt;
+use ctru::services::hid::{Hid, KeyPad};
+use ctru::services::soc::Soc;
+
+fn main() {
+    ctru::init();
+    let gfx = Gfx::init().expect("Couldn't obtain GFX controller");
+    let hid = Hid::init().expect("Couldn't obtain HID controller");
+    let apt = Apt::init().expect("Couldn't obtain APT controller");
+
+    let mut soc = Soc::init().expect("Couldn't obtain SOC controller");
+
+    soc.redirect_to_3dslink(true, true)
+        .expect("unable to redirect stdout/err to 3dslink server");
+
+    println!("Hello 3dslink!");
+    eprintln!("Press Start on the device to disconnect and exit.");
+
+    // Main loop
+    while apt.main_loop() {
+        //Scan all the inputs. This should be done once for each frame
+        hid.scan_input();
+
+        if hid.keys_down().contains(KeyPad::KEY_START) {
+            break;
+        }
+        // Flush and swap framebuffers
+        gfx.flush_buffers();
+        gfx.swap_buffers();
+
+        //Wait for VBlank
+        gfx.wait_for_vblank();
+    }
+}
diff --git a/ctru-rs/examples/thread-info.rs b/ctru-rs/examples/thread-info.rs
index 47ae486..337c49e 100644
--- a/ctru-rs/examples/thread-info.rs
+++ b/ctru-rs/examples/thread-info.rs
@@ -10,7 +10,7 @@ use std::os::horizon::thread::BuilderExt;
 
 fn main() {
     ctru::init();
-    let gfx = Gfx::default();
+    let gfx = Gfx::init().expect("Couldn't obtain GFX controller");
     let hid = Hid::init().expect("Couldn't obtain HID controller");
     let apt = Apt::init().expect("Couldn't obtain APT controller");
     let _console = Console::init(gfx.top_screen.borrow_mut());
diff --git a/ctru-rs/src/services/soc.rs b/ctru-rs/src/services/soc.rs
index 5705d06..e058516 100644
--- a/ctru-rs/src/services/soc.rs
+++ b/ctru-rs/src/services/soc.rs
@@ -10,6 +10,7 @@ use crate::services::ServiceReference;
 #[non_exhaustive]
 pub struct Soc {
     _service_handler: ServiceReference,
+    sock_3dslink: libc::c_int,
 }
 
 static SOC_ACTIVE: Lazy<Mutex<usize>> = Lazy::new(|| Mutex::new(0));
@@ -52,7 +53,10 @@ impl Soc {
             },
         )?;
 
-        Ok(Self { _service_handler })
+        Ok(Self {
+            _service_handler,
+            sock_3dslink: -1,
+        })
     }
 
     /// IP Address of the Nintendo 3DS system.
@@ -60,6 +64,38 @@ impl Soc {
         let raw_id = unsafe { libc::gethostid() };
         Ipv4Addr::from(raw_id.to_ne_bytes())
     }
+
+    /// Redirect output streams (i.e. [`println`] and [`eprintln`]) to the `3dslink` server.
+    /// Requires `3dslink` >= 0.6.1 and `new-hbmenu` >= 2.3.0.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if a connection cannot be established to the server, or
+    /// output was already previously redirected.
+    pub fn redirect_to_3dslink(&mut self, stdout: bool, stderr: bool) -> crate::Result<()> {
+        if self.sock_3dslink >= 0 {
+            // TODO AlreadyRedirected or something
+            return Err(crate::Error::ServiceAlreadyActive);
+        }
+
+        let sock = unsafe { ctru_sys::link3dsConnectToHost(stdout, stderr) };
+        if sock < 0 {
+            Err(sock.into())
+        } else {
+            self.sock_3dslink = sock;
+            Ok(())
+        }
+    }
+}
+
+impl Drop for Soc {
+    fn drop(&mut self) {
+        if self.sock_3dslink >= 0 {
+            unsafe {
+                libc::closesocket(self.sock_3dslink);
+            }
+        }
+    }
 }
 
 #[cfg(test)]
@@ -69,7 +105,7 @@ mod tests {
 
     #[test]
     fn soc_duplicate() {
-        let _soc = Soc::init().unwrap();
+        // let _soc = Soc::init().unwrap();
 
         assert!(matches!(Soc::init(), Err(Error::ServiceAlreadyActive)))
     }