Posts in Tutorial (20 found)
Rik Huijzer 2 days ago

How To Run Services on a Linux Server

I have been running services myself for a few years on Linux servers. It took a while to figure out what works best. Here's what I've learned. First of all, all maintenance is done on headless servers via SSH. Learning this might seem daunting for some at first, but it is truly unbeatable in terms of productivity and speed. To easily log in via SSH, add the SSH keys to the server and then add the server to your `~/.ssh/config`. For example, ``` Host arnold Hostname 123.456.789.012 User rik IdentityFile ~/.ssh/arnold ``` Now you can log in via `ssh arnold` instead of having to ma...

0 views
Langur Monkey 3 days ago

LM Studio on systemd linger

The release of LM Studio 0.4.5 has introduced a much needed feature in this local LLM suite that has it much more attractive with respect to other similar projects. LM Link allows you to connect multiple LM Studio instances across your network to share models and perform inference seamlessly. By sheer chance, I was just playing around with setting up an LM Studio server in an old laptop that I planned to use for inference. I would connect AnythingLLM clients to it to make API requests. The timing of 0.4.5 was perfect for me, as I could now use LM Studio for inference directly, and forget about using up my own Tailscale network. But some setup was needed in the laptop. To make this work effectively, the LM Studio server needs to run in the background, start automatically on boot, and persist even when I’m not logged in. The LM Studio website provides the source of a service file . It suggests creating it as a system-wide service, which is weird, as the default installation method (at least on Linux) sets everything up in the user home directory. I modified it a bit to make things clean, as I want this to be a user service. It keeps the process tied to your user environment but, with a little tweak called lingering, allows it to run without an active SSH or GUI session. Here is the setup. By default, user services stop the moment the user logs out. To prevent this and allow the LM Studio daemon to start at boot and stay alive, run: Then, create a directory for your user services if it doesn’t exist: After that, create a file named in that directory ( ), with the following contents: Once the file is saved, tell systemd to reload its configuration and enable the service: If you want to load a specific model by default, add an additional line, like this: You can check the service status with . And that is it. You can now use your old hardware for inference with small local LLMs from any of your other machines.

0 views
Jeff Geerling 3 days ago

How to Securely Erase an old Hard Drive on macOS Tahoe

Apparently Apple thinks nobody with a modern Mac uses spinning rust (hard drives with platters) anymore. I plugged in a hard drive from an old iMac into my Mac Studio using my Sabrent USB to SATA Hard Drive enclosure, and opened up Disk Utility, clicked on the top-level disk in the sidebar, and clicked 'Erase'. Lo and behold, there's no 'Security Options' button on there, as there had been since—I believe—the very first version of Disk Utility in Mac OS X!

0 views
(think) 5 days ago

Learning Vim in 3 Steps

Every now and then someone asks me how to learn Vim. 1 My answer is always the same: it’s simpler than you think, but it takes longer than you’d like. Here’s my bulletproof 3-step plan. Start with – it ships with Vim and takes about 30 minutes. It’ll teach you enough to survive: moving around, editing text, saving, quitting. The essentials. Once you’re past that, I strongly recommend Practical Vim by Drew Neil. This book changed the way I think about Vim. I had known the basics of Vim for over 20 years, but the Vim editing model never really clicked for me until I read it. The key insight is that Vim has a grammar – operators (verbs) combine with motions (nouns) to form commands. (delete) + (word) = . (change) + (inside quotes) = . Once you internalize this composable language, you stop memorizing individual commands and start thinking in Vim . The book is structured as 121 self-contained tips rather than a linear tutorial, which makes it great for dipping in and out. You could also just read cover to cover – Vim’s built-in documentation is excellent. But let’s be honest, few people have that kind of patience. Other resources worth checking out: Resist the temptation to grab a massive Neovim distribution like LazyVim on day one. You’ll find it overwhelming if you don’t understand the basics and don’t know how the Vim/Neovim plugin ecosystem works. It’s like trying to drive a race car before you’ve learned how a clutch works. Instead, start with a minimal configuration and grow it gradually. I wrote about this in detail in Build your .vimrc from Scratch – the short version is that modern Vim and Neovim ship with excellent defaults and you can get surprisingly far with a handful of settings. I’m a tinkerer by nature. I like to understand how my tools operate at their fundamental level, and I always take that approach when learning something new. Building your config piece by piece means you understand every line in it, and when something breaks you know exactly where to look. I’m only half joking. Peter Norvig’s famous essay Teach Yourself Programming in Ten Years makes the case that mastering any complex skill requires sustained, deliberate practice over a long period – not a weekend crash course. The same applies to Vim. Grow your configuration one setting at a time. Learn Vimscript (or Lua if you’re on Neovim). Read other people’s configs. Maybe write a small plugin. Every month you’ll discover some built-in feature or clever trick that makes you wonder how you ever lived without it. One of the reasons I chose Emacs over Vim back in the day was that I really hated Vimscript – it was a terrible language to write anything in. These days the situation is much better: Vim9 Script is a significant improvement, and Neovim’s switch to Lua makes building configs and plugins genuinely enjoyable. Mastering an editor like Vim is a lifelong journey. Then again, the way things are going with LLM-assisted coding, maybe you should think long and hard about whether you want to commit your life to learning an editor when half the industry is “programming” without one. But that’s a rant for another day. If this bulletproof plan doesn’t work out for you, there’s always Emacs. Over 20 years in and I’m still learning new things – these days mostly how to make the best of evil-mode so I can have the best of both worlds. As I like to say: The road to Emacs mastery is paved with a lifetime of invocations. That’s all I have for you today. Keep hacking! Just kidding – everyone asks me about learning Emacs. But here we are.  ↩︎ Advent of Vim – a playlist of short video tutorials covering basic Vim topics. Great for visual learners who prefer bite-sized lessons. ThePrimeagen’s Vim Fundamentals – if you prefer video content and a more energetic teaching style. vim-be-good – a Neovim plugin that gamifies Vim practice. Good for building muscle memory. Just kidding – everyone asks me about learning Emacs. But here we are.  ↩︎

0 views
(think) 5 days ago

How to Vim: Auto-save on Activity

Coming from Emacs, one of the things I missed most in Vim was auto-saving. I’ve been using my own super-save Emacs package for ages – it saves your buffers automatically when you switch between them, when Emacs loses focus, and on a handful of other common actions. After years of using it I’ve completely forgotten that exists. Naturally, I wanted something similar in Vim. Vim’s autocommands make it straightforward to set up basic auto-saving. Here’s what I ended up with: This saves the current buffer when Vim loses focus (you switch to another window) and when you leave Insert mode. A few things to note: You can extend this with more events if you like: Adding catches edits made in Normal mode (like , , or paste commands), so you’re covered even when you never enter Insert mode. works reliably in GUI Vim and most modern terminal emulators, but it may not fire in all terminal setups (especially inside tmux without additional configuration). One more point in favor of using Ghostty and not bothering with terminal multiplexers. The same autocommands work in Neovim. You can put the equivalent in your : Neovim also has ( ) which automatically saves before certain commands like , , and . It’s not a full auto-save solution, but it’s worth knowing about. There are several plugins that take auto-saving further, notably vim-auto-save for Vim and auto-save.nvim for Neovim. Most of these plugins rely on – an event that fires after the cursor has been idle for milliseconds. The problem is that is a global setting that also controls how often swap files are written, and other plugins depend on it too. Setting it to a very low value (say, 200ms) for snappy auto-saves can cause side effects – swap file churn, plugin conflicts, and in Neovim specifically, can behave inconsistently when timers are running. For what it’s worth, I think idle-timer-based auto-saving is overkill in Vim’s context. The simple autocommand approach covers the important cases, and anything more aggressive starts fighting against Vim’s grain. I’ve never been fond of the idle-timer approach to begin with, and that’s part of the reason why I created for Emacs. I like the predictability of triggering save by doing some action. Simplicity is the ultimate sophistication. – Leonardo da Vinci Here’s the thing I’ve come to appreciate about Vim: saving manually isn’t nearly as painful as it is in Emacs. In Emacs, is a two-chord sequence that you type thousands of times a day – annoying enough that auto-save felt like a necessity. In Vim, you’re already in Normal mode most of the time, so a quick mapping like: gives you a fast, single-keystroke save (assuming your leader is Space, which it should be). It’s explicit, predictable, and takes almost no effort. As always, I’ve learned quite a bit about Vim by looking into this simple topic. That’s probably the main reason I still bother to write such tutorial articles – they make me reinforce the knowledge I’ve just obtained and make ponder more than usual about the trade-offs between different ways to approach certain problems. I still use the autocommand approach myself – old habits die hard – but I have to admit that gets the job done just fine. Sometimes the simplest solution really is the best one. That’s all I have for you today. Keep hacking! instead of – it only writes when the buffer has actually changed, avoiding unnecessary disk writes. – suppresses errors for unnamed buffers and read-only files that can’t be saved.

0 views
Jim Nielsen 6 days ago

Making Icon Sets Easy With Web Origami

Over the years, I’ve used different icon sets on my blog. Right now I use Heroicons . The recommended way to use them is to copy/paste the source from the website directly into your HTML. It’s a pretty straightforward process: If you’re using React or Vue, there are also npm packages you can install so you can import the icons as components. But I’m not using either of those frameworks, so I need the raw SVGs and there’s no for those so I have to manually grab the ones I want. In the past, my approach has been to copy the SVGs into individual files in my project, like: Then I have a “component” for reading those icons from disk which I use in my template files to inline the SVGs in my HTML. For example: It’s fine. It works. It’s a lot of node boilerplate to read files from disk. But changing icons is a bit of a pain. I have to find new SVGs, overwrite my existing ones, re-commit them to source control, etc. I suppose it would be nice if I could just and get the raw SVGs installed into my folder and then I could read those. But that has its own set of trade-offs. For example: So the project’s npm packages don’t provide the raw SVGs. The website does, but I want a more programatic way to easily grab the icons I want. How can I do this? I’m using Web Origami for my blog which makes it easy to map icons I use in my templates to Heroicons hosted on Github. It doesn’t require an or a . Here’s an snippet of my file: As you can see, I name my icon (e.g. ) and then I point it to the SVG as hosted on Github via the Heroicons repo. Origami takes care of fetching the icons over the network and caching them in-memory. Beautiful, isn’t it? It kind of reminds me of import maps where you can map a bare module specifier to a URL (and Deno’s semi-abandoned HTTP imports which were beautiful in their own right). Origami makes file paths first-class citizens of the language — even “remote” file paths — so it’s very simple to create a single file that maps your icon names in a codebase to someone else’s icon names from a set, whether those are being installed on disk via npm or fetched over the internet. To simplify my example earlier, I can have a file like : Then I can reference those icons in my templates like this: Easy-peasy! And when I want to change icons, I simply update the entries in to point somewhere else — at a remote or local path. And if you really want to go the extra mile, you can use Origami’s caching feature: Rather than just caching the files in memory, this will cache them to a local folder like this: Which is really cool because now when I run my site locally I have a folder of SVG files cached locally that I can look at and explore (useful for debugging, etc.) This makes vendoring really easy if I want to put these in my project under source control. Just run the file once and boom, they’re on disk! There’s something really appealing to me about this. I think it’s because it feels very “webby” — akin to the same reasons I liked HTTP imports in Deno. You declare your dependencies with URLs, then they’re fetched over the network and become available to the rest of your code. No package manager middleman introducing extra complexity like versioning, transitive dependencies, install bloat, etc. What’s cool about Origami is that handling icons like this isn’t a “feature” of the language. It’s an outcome of the expressiveness of the language. In some frameworks, this kind of problem would require a special feature (that’s why you have special npm packages for implementations of Heroicons in frameworks like react and vue). But because of the way Origami is crafted as a tool, it sort of pushes you towards crafting solutions in the same manner as you would with web-based technologies (HTML/CSS/JS). It helps you speak “web platform” rather than some other abstraction on top of it. I like that. Reply via: Email · Mastodon · Bluesky Go to the website Search for the icon you want Click to “Copy SVG” Go back to your IDE and paste it Names are different between icon packs, so when you switch, names don’t match. For example, an icon might be named in one pack and in another. So changing sets requires going through all your templates and updating references. Icon packs are often quite large and you only need a subset. might install hundreds or even thousands of icons I don’t need.

0 views
(think) 1 weeks ago

How to Vim: Build your .vimrc from Scratch

People often think that getting started with Vim means spending hours crafting an elaborate with dozens of plugins. In reality, modern Vim (9+) and Neovim ship with remarkably sane defaults, and you can get very far with a configuration that’s just a few lines long – or even no configuration at all. If you launch Vim 9 without a file, it automatically loads – a built-in configuration that provides a solid foundation. Here’s what you get for free: That’s actually a pretty reasonable editing experience out of the box! You can read the full details with . Neovim goes even further with its defaults – it enables (copies indentation from the previous line), (highlights all search matches), (makes Tab smarter at the start of a line), (reloads files changed outside the editor), always shows the statusline, and sets the command history to 10000 entries, among many other things. If you’re on Neovim, the out-of-the-box experience is excellent. See for the full list. Here’s something that trips up a lot of people: the moment you create a file – even an empty one – Vim stops loading entirely. That means you lose all those nice defaults. The fix is simple. Start your with: This loads the defaults first, and then your own settings override or extend them as needed. This gotcha only applies to Vim. Neovim’s defaults are always active regardless of whether you have an or . Here’s a minimal that builds on the defaults and adds a few things most people want: That’s five settings on top of the defaults. You might not even need all of them – already handles the fundamentals. For Neovim, you don’t need the line – all the equivalents are already active. You also get , , and for free, so the only settings left to add are the ones that are genuinely personal preference: One of the most underappreciated aspects of Vim is how much built-in support it ships for programming languages. When is active (which it is via or Neovim’s defaults), you automatically get: This means that when you open a Python file, Vim already knows to use 4-space indentation. Open a Ruby file and it switches to 2 spaces. Open a Makefile and it uses tabs. All without a single plugin or line of configuration. You can check what’s available with for syntax files or for filetype plugins. The list is impressively long. At some point you’ll probably want more than the bare minimum. Here are a few things worth considering as your next steps: And when you eventually want more plugins, you probably won’t need many. A fuzzy finder, maybe a Git integration, and perhaps a completion engine will cover most needs. But that’s a topic for another day. The key takeaway is this: don’t overthink your . Start with the defaults, add only what you actually need, and resist the urge to copy someone else’s 500-line configuration. A small, well-understood configuration beats a large, cargo-culted one every time. That’s part of the reason why when I started to re-learn Vim I’ve opted to slowly build a Vim 9 configuration from scratch, instead of jumping to something like Neovim + Kickstart.nvim or LazyVim right away. Less is more. Understanding the foundations of your editor matters. 1 Right now my is just 100 lines and I don’t foresee it becoming much bigger in the long run. If you want to see just how far you can go without plugins, I highly recommend the Thoughtbot talk How to Do 90% of What Plugins Do (With Just Vim) . It’s a great demonstration of Vim’s built-in capabilities for file finding, auto-completion, tag navigation, and more. That’s all I have for you today. Keep hacking! I guess this sounds strange coming from the author of Emacs Prelude, right?  ↩︎ – syntax highlighting – filetype detection, language-specific plugins, and automatic indentation – incremental search (results appear as you type) – keeps 5 lines of context around the cursor – shows instead of hiding truncated lines – mouse support in all modes remapped to (text formatting) instead of the mostly useless Ex mode And several other quality-of-life improvements Syntax highlighting for hundreds of languages – Vim ships with around 770+ syntax definitions Language-specific indentation rules for over 420 file types Filetype plugins that set sensible options per language (e.g., , , ) A colorscheme – Vim ships with several built-in options (try followed by Tab to see them). Recent Vim builds even bundle Catppuccin – a beautiful pastel theme that I’m quite fond of. Another favorite of mine is Tokyo Night , which you’ll need to install as a plugin. Neovim’s default colorscheme has also been quite good since 0.10. Persistent undo – lets you undo changes even after closing and reopening a file. A game changer. Clipboard integration – makes yank and paste use the system clipboard by default. vim-unimpaired – if you’re on classic Vim (not Neovim), I think Tim Pope’s vim-unimpaired is essential. It adds a consistent set of / mappings for navigating quickfix lists, buffers, adding blank lines, and much more. Neovim 0.11+ has adopted many of these as built-in defaults, but on Vim there’s no substitute. I guess this sounds strange coming from the author of Emacs Prelude, right?  ↩︎

0 views
(think) 1 weeks ago

Adding Empty Lines in Vim: Redux

A long time ago I wrote about adding empty lines in Emacs on my other blog, Emacs Redux. Now it’s time for the Vim version of this. Adding a blank line above or below the cursor is one of those tiny operations you do constantly, and Vim gives you several ways to do it – each with different trade-offs. Most Vim users reach for (open line below) or (open line above). These work, but they drop you into Insert mode. If you just want a blank line and want to stay in Normal mode, you need or . This gets the job done, but has a couple of annoyances: On the bright side, / accept a count – inserts 5 blank lines below. A lesser-known approach that avoids the issues above: The here is Vim’s black hole register, which is always empty. Since inserts register contents linewise, putting “nothing” results in a clean blank line. No trailing whitespace, no register pollution, and you stay in Normal mode. (using the expression register) works the same way, but is shorter and easier to remember. The downside? It’s verbose to type interactively, and the cursor still moves to the new line. This is the gold standard: They also accept a count – adds 3 blank lines below. In Neovim 0.11+ , these are built-in default mappings. In regular Vim, you need Tim Pope’s vim-unimpaired plugin. Note that the two implementations differ slightly – Neovim uses which preserves the cursor position exactly, while vim-unimpaired uses with mark jumps, which keeps you on the same line but resets the column to 0. If you’re on plain Vim without plugins, you can add equivalent mappings to your : The approach is the cleanest – no side effects on registers, marks, or undo granularity. In Insert mode your options are simpler: is handy when you want to add a line without breaking your Insert mode flow. My recommendation? Use and . Like most Vim users, I relied on and for years, but once I discovered vim-unimpaired (and later the equivalent built-in mappings in Neovim 0.11) I never looked back. More broadly, I’m a huge fan of the uniform / convention that vim-unimpaired pioneered – / for quickfix, / for buffers, / for blank lines, and so on. It’s a consistent, mnemonic system that’s easy to internalize, and I’m glad Neovim has been adopting it as built-in defaults. If you’re on Vim, do yourself a favor and install vim-unimpaired . That’s all I have for you today. Keep hacking! The cursor moves to the new blank line. Auto-indent may leave trailing whitespace on the “blank” line. It pollutes the repeat register (it records an insert operation). – adds a blank line below the current line – adds a blank line above the current line – adds a blank line below – adds a blank line above – creates a new line below (or splits the line if the cursor is in the middle of text). – executes as a one-shot Normal mode command, opening a line below. You end up in Insert mode on the new line. – same thing, but opens a line above.

0 views
Takuya Matsuyama 1 weeks ago

How to run Claude Code in a Tmux popup window with persistent sessions

Hey, what's up? It's Takuya. I've been using Claude Code in my terminal workflow. At first, I was running it at the right side of my terminal using tmux, but I found it not useful because it was too narrow to display messages and diffs. I often had to press to maximize the pane, which was painful. Next, I started using popup windows to run Claude Code — press a keybinding, get a Claude Code session, dismiss it, and pick up right where you left off. In this article, I'd like to share how to configure tmux to accomplish it. You can display popup windows with command, which is great for quick-access tools. I've been using it for quickly checking git status with like this: My prefix key is , so it is bound to . This works perfectly for LazyGit because it's a short-lived process — you open it, stage some changes, commit, and close it. However, there is a problem with running Claude Code (or any other AI tools) in tmux popup windows. You want to keep a conversation going across multiple interactions. If you bind Claude Code the same way: ...you'll find that closing the popup also kills the Claude Code process. There's no way to dismiss the popup without quitting the session. You'd have to start fresh every time, which defeats the purpose. The trick is to run Claude Code in a separate tmux session, and then attach to that session inside the popup, which means that you are going to use nested tmux sessions. When you close the popup, the session keeps running in the background. Here's the full configuration: Let's break down what this does: This takes the current pane's working directory, hashes it with MD5, and uses the first 8 characters as a session identifier. So you get session names like . The key insight here is that each directory gets its own Claude Code session. The check prevents creating duplicate sessions. If a session for this directory already exists, it skips creation entirely. Otherwise, it creates a new detached session ( ) with the working directory set to your current path ( ), running Claude Code as the initial command. This opens an 80%-sized popup that attaches to the background session. You can change it as you like. When you close the popup (with or your detach keybinding), the session stays alive. Yippee! My dotfiles are available here: That's it. A very simple and intuitive hack. I hope it's helpful for your AI coding workflow :) Have a productive day! Generate a unique session name from the working directory Create the session if it doesn't already exist Attach to the session in a popup

0 views
xenodium 1 weeks ago

Bending Emacs - Episode 11: winpulse

I recently built a little package to flash Emacs windows as you switch through them, so I might as well showcase it in a new Bending Emacs episode, so here it goes: Bending Emacs Episode 11: winpulse In addition to showcasing winpulse , we showed some of the built-in window-managing commands like: It's worth noting the last four commands are can be optimized by repeat-mode . Check out Karthink's It Bears Repeating: Emacs 28 & Repeat Mode post. Hope you enjoyed the video! Liked the video? Please let me know. Got feedback? Leave me some comments . Please go like my video , share with others, and subscribe to my channel . If there's enough interest, I'll continue making more videos! Enjoying this content or my projects ? I am an indie dev. Help make it sustainable by ✨ sponsoring ✨ Need a blog? I can help with that . Maybe buy my iOS apps too ;) split-window-right split-window-below delete-window enlarge-window enlarge-window-horizontally shrink-window-horizontally other-window

0 views
Jimmy Miller 1 weeks ago

Untapped Way to Learn a Codebase: Build a Visualizer

The biggest shock of my early career was just how much code I needed to read that others wrote. I had never dealt with this. I had a hard enough time understanding my own code. The idea of understanding hundreds of thousands or even millions of lines of code written by countless other people scared me. What I quickly learned is that you don't have to understand a codebase in its entirety to be effective in it. But just saying that is not super helpful. So rather than tell, I want to show. In this post, I'm going to walk you through how I learn an unfamiliar codebase. But I'll admit, this isn't precisely how I would do it today. After years of working on codebases, I've learned quite a lot of shortcuts. Things that come with experience that just don't translate for other people. So what I'm going to present is a reconstruction. I want to show bits and parts of how I go from knowing very little to gaining knowledge and ultimately, asking the right questions. To do this, I will use just a few techniques: I want to do this on a real codebase, so I've chosen one whose purpose and scope I'm generally familiar with. But one that I've never contributed to or read, Next.js . But I've chosen to be a bit more particular than that. I'm particularly interested in learning more about the Rust bundler setup (turbopack) that Next.js has been building out. So that's where we will concentrate our time. Trying to learn a codebase is distinctly different from trying to simply fix a bug or add a feature. In post, we may use bugs, talk about features, make changes, etc. But we are not trying to contribute to the codebase, yet. Instead, we are trying to get our mind around how the codebase generally works. We aren't concerned with things like coding standards, common practices, or the development roadmap. We aren't even concerned with correctness. The changes we make are about seeing how the codebase responds so we can make sense of it. I find starting at to be almost always completely unhelpful. From main, yes, we have a single entry point, but now we are asking ourselves to understand the whole. But things actually get worse when dealing with a large codebase like this. There isn't even one main. Which main would we choose? So instead, let's start by figuring out what our library even consists of. A couple of things to note. We have packages, crates, turbo, and turbopack. Crates are relevant here because we know we are interested in some of the Rust code, but we are also interested in turbopack in particular. A quick look at these shows that turbo, packages, and crates are probably not our target. Why do I say that? Because turbopack has its own crates folder. So there are 54 crates under turbopack.... This is beginning to feel a bit daunting. So why don't we take a step back and find a better starting point? One starting point I find particularly useful is a bug report . I found this by simply looking at recently opened issues. When I first found it, it had no comments on it. In fact, I find bug reports with only reproducing instructions to be the most useful. Remember, we are trying to learn, not fix a bug. So I spent a little time looking at the bug report. It is fairly clear. It does indeed reproduce. But it has a lot of code. So, as is often the case, it is useful to reduce it to the minimal case. So that's what I did. Here is the important code and the problem we are using to learn from. MyEnum here is dead code. It should not show up in our final bundle. But when we do and look for it, we get: If we instead do The code is completely gone from our build. So now we have our bug. But remember. Our goal here is not to fix the bug. But to understand the code. So our goal is going to be to use this little mini problem to understand what code is involved in this bug. To understand the different ways we could fix this bug. To understand why this bug happened in the first place. To understand some small slice of the turbopack codebase. So at each junction, we are going to resist the urge to simply find the offending code. We are going to take detours. We are going to ask questions. We hope that from the start of this process to the end, we no longer think of the code involved in this action as a black box. But we will intentionally leave ourselves with open questions. As I write these words, I have no idea where this will take us. I have not prepared this ahead of time. I am not telling you a fake tale from a codebase I already know. Yes, I will simplify and skip parts. But you will come along with me. The first step for understanding any project is getting some part of it running. Well, I say that, but in my day job, I've been at companies where this is a multi-day or week-long effort. Sometimes, because of a lack of access, sometimes from unclear instructions, if you find yourself in that situation, you now have a new task, understand it well enough to get it to build. Well, unfortunately, that is the scenario we find ourselves in. I can't think of a single one of these endeavors I've gone on to learn a codebase that didn't involve a completely undesirable, momentum-stopping side quest. For this one, it was as soon as I tried to make changes to the turbopack Rust code and get it working in my test project. There are instructions on how to do this . In fact, we even get an explanation on why it is a bit weird. Since Turbopack doesn't support symlinks when pointing outside of the workspace directory, it can be difficult to develop against a local Next.js version. Neither nor imports quite cut it. An alternative is to pack the Next.js version you want to test into a tarball and add it to the pnpm overrides of your test application. The following script will do it for you: Okay, straightforward enough. I start by finding somewhere in the turbopack repo that I think will be called more than once, and I add the following: Yes. Very scientific, I know. But I've found this to be a rather effective method of making sure my changes are picked up. So I do that, make sure I've built and done the necessary things. I run Then that script tells me to add some overrides and dependencies to my test project. I go to build my project and HERERE!!!!!!! does not show up at all... I will save you the fun details here of looking through this system. But I think it's important to mention a few things. First, being a dependency immediately stood out to me. In my day job, I maintain a fork of swc (WHY???) for some custom stuff. I definitely won't pretend to be an expert on swc, but I know it's written in Rust. I know it's a native dependency. The changes I'm making are native dependencies. But I see no mention at all of turbopack. In fact, if I search in my test project, I get the following: So I have a sneaking suspicion my turbopack code should be in that tar. So let's look at the tar. Ummm. That seems a bit small... Let's look at what's inside. Okay, I think we found our problem. There's really nothing in this at all. Definitely no native code. After lots of searching, the culprit came down to: In our case, the input came from this file and f was . Unfortunately, this little set + regex setup causes to be filtered out. Why? Because it doesn't match the regex. This regex is looking for a with characters after it. We have none. So since we are already in the set (we just added ourselves), we filter ourselves out. How do we solve this problem? There are countless answers, really. I had Claude whip me up one without regex. But my gut says the sorting lets us do this much simpler. But after this fix, let's look at the tar now: Much better. After this change, we can finally see HERERE!!!!!!! a lot. Update : As I wrote this article, someone fixed this in a bit of a different way . Keeping the regex and just changing to . Fairly practical decision. Okay, we now have something we can test. But where do we even begin? This is one reason we chose this bug. It gives a few avenues to go down. First, the report says that these enums are not being "tree-shaken." Is that the right term? One thing I've learned from experience is to never assume that the end user is using terms in the same manner as the codebase. So this can be a starting point, but it might be wrong. With some searching around, we can actually see that there is a configuration for turning turbopackTreeShaking on or off. It was actually a bit hard to find exactly where the default for this was. It isn't actually documented. So let's just enable it and see what we get. Well, I think we figured out that the default is off. So one option is that we never "tree shake" anything. But that seems wrong. At this point, I looked into tree shaking a bit in the codebase, and while I started to understand a few things, I've been at this point before. Sometimes it is good to go deep. But how much of this codebase do I really understand? If tree shaking is our culprit (seeming unlikely at this point), it might be good to know how code gets there. Here, we of course found a bug. But it is an experimental feature. Maybe we can come back and fix it? Maybe we can file a bug? Maybe this code just isn't at all ready for primetime. It's hard to know as an outsider. Our "search around the codebase" strategy failed. So now we try a different tactic. We know a couple of things. We now have two points we can use to try to trace what happens. Let's start with parsing. Luckily, here it is straightforward: . When we look at this code, we can see that swc does the heavy lifting. First, it parses it into a TypeScript AST, then applies transforms to turn it into JavaScript. At this point, we don't write to a string, but if you edit the code and use an emitter, you see this: Now, to find where we write the chunks. In most programs, this would be pretty easy. Typically, there is a linear flow somewhere that just shows you the steps. Or if you can't piece one together, you can simply breakpoint and follow the flow. But Turbopack is a rather advanced system involving async Rust (more on this later). So, in keeping with the tradition of not trying to do things that rely too heavily on my knowledge, I have done the tried and true, log random things until they look relevant. And what I found made me realize that logging was not going to be enough. It was time to do my tried and true learning technique, visualization. Ever since my first job , I have been building custom tools to visualize codebases. Perhaps this is due to my aphantasia. I'm not really sure. Some of these visualizers make their way into general use for me. But more often than not, they are a means of understanding. When I applied for a job at Shopify working on YJIT, I built a simple visualizer but never got around to making it more useful than a learning tool. The same thing is true here, but this time, thanks to AI, it looks a bit more professional. This time, we want to give a bit more structure than what we'd be able to do with a simple print. 1 We are trying to get events out that have a bunch of information. Mostly, we are interested in files and their contents over time. Looking through the codebase, we find that one key abstract is an ident; this will help us identify files. We will simply find points that seem interesting, make a corresponding event, make sure it has idents associated with it, and send that event over a WebSocket. Then, with that raw information, we can have our visualizer stitch together what exactly happens. If we take a look, we can see our code step through the process. And ultimately end up in the bundle despite not being used. If you notice, though, between steps 3 and 4, our code changed a bit. We lost this PURE annotation. Why? Luckily, because we tried to capture as much context as we could. We can see that a boolean "Scope Hoisting" has been enabled. Could that be related? If we turn it off, we instead see: Our pure annotation is kept around, and as a result, our code is eliminated. If we take a step back, this can make sense. Something during the parse step is creating a closure around our enum code, but when it does so, it is marking that as a "pure" closure, meaning it has no side effects. Later, because this annotation is dropped, the minifier doesn't know that it can just get rid of this closure. As I've been trying to find time to write this up, it seems that people on the bug report have found this on their own as well. So we've found the behavior of the bug. Now we need to understand why it is happening. Remember, we are fixing a bug as a means of understanding the software. Not just to fix a bug. So what exactly is going on? Well, we are trying to stitch together two libraries. Software bugs are way more likely to occur on these seams. In this case, after reading the code for a while, the problem becomes apparent. SWC parses our code and turns it into an AST. But if you take a look at an AST , comments are not a part of the AST. So instead, SWC stores comments off in a hashmap where we can look them up by byte pos. So for each node in the AST, it can see if there is a comment attached. But for the PURE comment, it doesn't actually need to look this comment up. It is not a unique comment that was in the source code. It is a pre-known meta comment. So rather than store each instance in the map, it makes a special value. Now, this encoding scheme causes some problems for turbopack. Turbopack does not act on a single file; it acts across many files. In fact, for scope hoisting, we are trying to take files across modules and condense them into a single scope. So now, when we encounter one of these bytepos encodings, how do we know what module it belongs to? The obvious answer to many might be to simply make a tuple like , and while that certainly works, it does come with tradeoffs. One of these is memory footprint. I didn't find an exact reason. But given the focus on performance on turbopack, I'd imagine this is one of the main motivations. Instead, we get a fairly clever encoding of module and bytepos into a single BytePos, aka a u32. I won't get into the details of the representation here; it involves some condition stuff. But needless to say, now that we are going from something focusing on one file to focusing on multiple and trying to smuggle in this module_id into our BytePos, we ended up missing one detail, PURE. Now our pure value is being interpreted as some module at some very high position instead of the proper bytes. To fix this bug, I found the minimal fix was simply the following: With this our enum properly is marked as PURE and disappears from the output! Now remember, we aren't trying to make a bug fix. We are trying to understand the codebase. Is this the right fix? I'm not sure. I looked around the codebase, and there are a number of other swc sentinel values that I think need to also be handled (PLACEHOLDER and SYNTHESIZED). There is also the decoding path. For dummy, the decoding path panics. Should we do the same? Should we be handling pure at a higher level, where it never even goes through the encoder? Update : As I was writing this, someone else proposed a fix . As I was writing the article, I did see that others had started to figure out the things I had determined from my investigation. But I was not confident enough that it was the right fix yet. In fact, the PR differs a bit from my local fix. It does handle the other sentinel, but at a different layer. It also chooses to decode with module 0. Which felt a bit wrong to me. But again, these are decisions that people who work on this codebase long-term are better equipped to decide than me. I must admit that simply fixing this bug didn't quite help me understand the codebase. Not just because it is a fairly good size. But because I couldn't see this fundamental unit that everything was composed of. In some of the code snippets above, you will see types that mention Vc. This stands for ValueCell. There are a number of ways to try to understand these; you can check out the docs for turbo engine for some details. Or you can read the high-level overview that skips the implementation details for the most part. You can think of these cells like the cells in a spreadsheet. They provide a level of incremental computation. When the value of some cell updates, we can invalidate stuff. Unlike a spreadsheet, the turbo engine is lazy. I've worked with these kinds of systems before. Some are very explicitly modeled after spreadsheets. Others are based on rete networks or propagators. I am also immediately reminded of salsa from the Rust analyzer team. I've also worked with big, complicated non-computational graphs. But even with that background, I know myself, I've never been able to really understand these things until I can visualize them. And while a general network visualizer can be useful (and might actually be quite useful if I used the aggregate graph), I've found that for my understanding, I vastly prefer starting small and exploring out the edges of the graph. So that's what I did. But before we get to that visualization, I want to highlight something fantastic in the implementation: a central place for controlling a ton of the decisions that go into this system. The backend here lets us decide so many things about how the execution of our tasks will run. Because of this, we have one place we can insert a ton of tooling and begin to understand how this system works. As before, we are going to send things on a WebSocket. But unlike last time, our communication will actually be two-way. We are going to be controlling how the tasks run. In my little test project, I edited a file, and my visualizer displayed the following. Admittedly, it is a bit janky, but there are some nice features. First, on the left, we can see all the pending tasks. In this case, something has marked our file read as dirty, so we are trying to read the file. We can see the contents of a cell that this task has. And we can see the dependents of this task. Here is what it looks like once we release that task to run. We can now see 3 parse tasks have kicked off. Why 3? I'll be honest, I haven't looked into it. But a good visualization is about provoking questions, not only answering them. Did I get my visualization wrong because I misunderstood something about the system? Are there three different subsystems that want to parse, and we want to do them in parallel? Have we just accidentally triggered more parses than we should be? This is precisely what we want out of a visualizer. Is it perfect? No, would I ship this as a general visualizer? No. Am I happy with the style? Not in the least. But already it enables a look into the project I couldn't see before. Here we can actually watch the graph unfold as I execute more steps. What a fascinating view of a once opaque project. With this visualizer, I was able to make changes to my project and watch values as they flow through the systems. I made simple views for looking at code. If I extended this, I can imagine it being incredibly useful for debugging general issues, for seeing the ways in which things are scheduled, and for finding redundancies in the graph. Once I was able to visualize this, I really started to understand the codebase better. I was able to see all the values that didn't need to be recomputed when we made changes. The whole thing clicked. This was an exercise in exploring a new codebase that is a bit different of a process than I see others take. It isn't an easy process, it isn't quick. But I've found myself repeating this process over and over again. For the turborepo codebase, this is just the beginning. This exploration was done over a few weekends in the limited spare time I could find. But already I can start to put the big picture together. I can start to see how I could shape my tools to help me answer more questions. If you've never used tool building as a way to learn a codebase, I highly recommend it. One thing I always realize as I go through this process is just how hard it is to work interactively with our current software. Our languages, our tools, our processes are all written without ways to live code, without ways to inspect their inner workings. It is also incredibly hard to find a productive UI environment for this kind of live exploration. The running state of the visualizer contains all the valuable information. Any system that needs you to retrace your steps to get the UI back to the state it was once in to visualize more is incredibly lacking. So I always find myself in the browser, but immediately, I am having to worry about performance. We have made massive strides in so many aspects of software development. I hope that we will fix this one as well. Setting a goal Editing randomly Fixing things I find that are broken Reading to answer questions Making a visualizer Our utilities.ts file is read and parsed. It ends up in a file under a "chunks" directory.

0 views
Danny McClelland 2 weeks ago

Running My Own XMPP Server

About a year ago I moved my personal messaging to Signal as part of a broader push to take ownership of my digital life. That went well. Most of my contacts made the switch, and I’m now at roughly 95% Signal for day-to-day conversations. But Signal is still one company running one service. If they shut down tomorrow or change direction, I’m back to square one. XMPP fixes that. It’s federated, meaning your server talks to other XMPP servers automatically and you’re never locked into a single provider. Your messages live on your hardware. The protocol has been around since 1999 and it’s not going anywhere. I’d tried XMPP years ago and bounced off it, but the clients have come a long way since then. Monal and Conversations are genuinely nice to use now. This post covers everything I did to get a fully working XMPP server running with Prosody in Docker, from DNS records through to voice calls. XMPP uses SRV records to let clients and other servers find yours. You’ll need these in your DNS: Port 5222 is for client connections, 5269 is for server-to-server federation. You’ll also want an A record pointing to your server’s IP. If you want HTTP file uploads (I’d recommend it), add a CNAME or A record for pointing to the same server. Same for if you want group chats with a clean subdomain, though Prosody handles this internally either way. Prosody won’t start without certificates. I use Let’s Encrypt with the Cloudflare DNS challenge so I don’t need to expose port 80: The file contains your API token: After certbot runs, fix the permissions so Prosody can read the certs: Set up a cron to renew monthly: Two ports exposed: 5222 for clients, 5269 for federation. The data volume holds user accounts and message archives. Config and certs are mounted read-only. This is the core of it. I’ll walk through the key sections rather than dumping the whole file. Prosody is modular. My module list: The ones I found matter most for a good mobile experience: syncs messages across all your devices instead of delivering to whichever one happened to be online. (Stream Management) handles flaky connections gracefully, so messages aren’t lost when your phone briefly drops signal. enables push notifications so mobile clients don’t need a persistent connection, which is essential for battery life. And (Message Archive Management) stores history server-side for search and cross-device sync. All connections are encrypted and registration is disabled since I create accounts manually with . I’ve enabled , which means Prosody will reject connections from servers with self-signed or misconfigured certificates. You’ll lose federation with some poorly configured servers, but if you’re self-hosting for privacy reasons it doesn’t make much sense to relax authentication for other people’s mistakes. TLS encrypts connections in transit, but the server itself can still read your messages. If you’re self-hosting, that means you’re trusting yourself, which is fine. But if other people use your server, or if you just want the belt-and-braces approach, OMEMO adds end-to-end encryption so that not even the server operator can read message content. OMEMO is built on the same encryption that Signal uses, so I’m comfortable trusting it. There’s nothing to configure on the server side either. OMEMO is handled entirely by the clients. Monal, Conversations, and Gajim all support it, and in most cases it’s enabled by default for new conversations. I’d recommend turning it on for everything and leaving it on. Messages are kept for a year and archiving is on by default. Clients can opt out per-conversation if they want. Prosody serves HTTP on port 5280 internally. I leave HTTPS to my reverse proxy (Caddy), which handles TLS termination. The tells Prosody what URL to hand clients when they upload files. The MUC (Multi-User Chat) component gives you group chats with message history via . I restrict room creation to local users so random federated accounts can’t spin up rooms on my server. The file share component handles image and file uploads. A 10 MB limit and 30-day expiry keeps disk usage under control. Prosody’s HTTP port needs to be reachable from the internet for file uploads to work. I use Caddy: When a client sends an image, Prosody hands it a URL like and the receiving client fetches it over HTTPS. With registration disabled, accounts are created from the command line: It prompts for a password. Done. Log in from any XMPP client. Open the XMPP ports: Port 80/443 for the reverse proxy if you haven’t already. If your server is behind a router, forward 5222 and 5269. Text and file sharing work at this point. Voice and video calls need one more piece: a TURN/STUN server. Without it, clients behind NAT can’t establish direct media connections. I run coturn alongside Prosody. The two share a secret, and Prosody generates temporary credentials for clients automatically. Generate a shared secret: The coturn : It runs with because TURN needs real network interfaces to handle NAT traversal. Docker’s port mapping breaks this. If your server is behind NAT, add: Then tell Prosody about it. Add to your modules, and inside the block: Open the firewall ports: Verify with . On iOS I went with Monal , which is open source and supports all the modern XEPs. Push notifications work well. On Android, Conversations seems to be the go-to. On desktop, Gajim covers Linux and Windows, and Monal has a macOS build. All of them support OMEMO encryption, file sharing, group chats, and voice/video calls. Prosody has solid built-in diagnostics: This checks DNS records, TLS certificates, connectivity, and module configuration. Fix anything it flags. The error messages are genuinely helpful. The XMPP Compliance Tester is worth running too. Mine scored above 90% after getting the config right. The whole setup runs in two small Docker containers and a reverse proxy entry. Prosody, file uploads, message archive, push notifications, group chats, voice calls. I still use Signal for most day-to-day conversations and I’m not planning to stop. But having my own XMPP server means I’m not entirely dependent on any single service. I can message anyone on any XMPP server, not just people who signed up to the same one. It’s a nice fallback to have. If you’re already running Docker on a server somewhere, it’s a good weekend project. A server with Docker and Docker Compose A domain you control TLS certificates (Let’s Encrypt works well)

0 views
(think) 2 weeks ago

How to Vim: Many Ways to Paste

Most Vim users know and – paste after and before the cursor. Simple enough. But did you know that Vim actually has around a dozen paste commands, each with subtly different behavior? I certainly didn’t when I started using Vim, and I was surprised when I discovered the full picture. Let’s take a tour of all the ways to paste in Vim, starting with Normal mode and then moving to Insert mode. One important thing to understand first – it’s all about the register type . Vim registers don’t just store text, they also track how that text was yanked or deleted. There are three register types (see ): This is something that trips up many Vim newcomers – the same command can behave quite differently depending on the register type! With that in mind, here’s the complete family of paste commands in Normal mode: The “Direction” column above reflects both cases – for characterwise text it’s “after/before the cursor”, for linewise text it’s “below/above the current line”. How to pick the right paste command? Here are a few things to keep in mind: All Normal mode paste commands accept a count (e.g., pastes three times) and a register prefix (e.g., pastes from register ). In Insert mode things get interesting. All paste commands start with , but the follow-up keystrokes determine how the text gets inserted: Let me unpack this a bit: Note: Plain can be a minor security concern when pasting from the system clipboard ( or registers), since control characters in the clipboard will be interpreted. When in doubt, use instead. And that’s a wrap! Admittedly, even I didn’t know some of those ways to paste before doing the research for this article. I’ve been using Vim quite a bit in the past year and I’m still amazed how many ways to paste are there! If you want to learn more, check out , , , and in Vim’s built-in help. There’s always more to discover! That’s all I have for you today. Keep hacking! You can also use and with a register, e.g. to paste from register with adjusted indentation.  ↩ Characterwise (e.g., ): inserts text to the right of the cursor, to the left. Linewise (e.g., , ): inserts text on a new line below, on a new line above. The cursor position within the line doesn’t matter. Blockwise (e.g., selection): text is inserted as a rectangular block starting at the cursor column. The difference between / and / is all about where your cursor ends up. With the cursor lands on the last character of the pasted text, while with it moves just past the pasted text. This makes handy when you want to paste something and continue editing right after it. and are incredibly useful when pasting code – they automatically adjust the indentation of the pasted text to match the current line. No more pasting followed by to fix indentation! 1 and are the most niche – they only matter when pasting blockwise selections, where they avoid adding trailing whitespace to pad shorter lines. is the most common one – you press and then a register name (e.g., , , for the system clipboard). The text is inserted as if you typed it, which means and auto-indentation apply. This can be surprising if your pasted code gets reformatted unexpectedly. inserts the text literally – special characters like backspace won’t be interpreted. However, and auto-indent still apply. is the “raw paste” – no interpretation, no formatting, no auto-indent. What you see in the register is what you get. This is the one I’d recommend for pasting code in Insert mode. is like , but it adjusts the indentation to match the current context. Think of it as the Insert mode equivalent of . You can also use and with a register, e.g. to paste from register with adjusted indentation.  ↩

0 views
NULL on error 2 weeks ago

Hiding information inside images

As a child, there was a game called secret message which consisted of writing something on a sheet of paper with lemon juice and handing it to a friend who knew that by holding a candle underneath, a message would be revealed — without any of the intermediaries ever knowing. We can say that this game was a form of steganography “Steganography (from the Greek ‘hidden writing’) is the study and use of techniques to conceal the existence of a message within another, a form of security through obscurity. In other words, steganography is the particular branch of cryptology that consists of disguising one piece of writing within another in order to mask its true meaning.” - Wikipedia Today I’m going to introduce another type of steganography — one that uses images to hide text inside pixels! An image is composed of pixels, and each pixel represents the smallest possible color point using N bits. The more bits per color, the more colors a pixel can represent. In my example I will focus only on 32-bit images ( RGBA or ARGB ) where 8 bits are used per color component. Obviously the technique is easily adaptable to other formats. What happens if I change a single bit of the red component of every pixel? Even with the original image side by side, we would be unable to notice the difference — only a hashing algorithm like SHA1 could tell the files apart. Therefore, I can use this same trick of altering pixels to hide a message inside the image, and only someone who knows the implementation will know how to extract the message. But to do this we need to separate each bit of the message and change only one or two bits of each color component. It sounds like very little space, but if we do the math, an 800×600 image — which is considered low resolution today — can hold 800 width _ 600 height = 480000 pixels 480000 pixels _ 4 components = 1920000 bits 1920000 bits / 8 = 240000 bytes In other words, we can fit a message of up to ~30 KB in an 800×600 image :) Remember, we are talking about pixels, not file size — that depends on the chosen format. And speaking of format, this only works with lossless formats. To achieve our goal we need to master the ancient art of bit twiddling . There is an excellent resource on the subject called: Bit Twiddling Hacks Writing a hidden message: Reading a hidden message from an image: The file size changed slightly due to the format used — in this case, PNG. This technique can be used to hide not only text, but also other images, sounds, or any kind of data. The source code can be found in this gist

0 views
Anton Zhiyanov 2 weeks ago

Allocators from C to Zig

An allocator is a tool that reserves memory (typically on the heap) so a program can store its data structures there. Many C programs use the standard libc allocator, or at best, let you switch it out for another one like jemalloc or mimalloc. Unlike C, modern systems languages usually treat allocators as first-class citizens. Let's look at how they handle allocation and then create a C allocator following their approach. Rust • Zig • Odin • C3 • Hare • C • Final thoughts Rust is one of the older languages we'll be looking at, and it handles memory allocation in a more traditional way. Right now, it uses a global allocator, but there's an experimental Allocator API implemented behind a feature flag (issue #32838 ). We'll set the experimental API aside and focus on the stable one. The documentation begins with a clear statement: In a given program, the standard library has one "global" memory allocator that is used for example by and . Followed by a vague one: Currently the default global allocator is unspecified. It doesn't mean that a Rust program will abort an allocation, of course. In practice, Rust uses the system allocator as the global default (but the Rust developers don't want to commit to this, hence the "unspecified" note): The global allocator interface is defined by the trait in the module. It requires the implementor to provide two essential methods — and , and provides two more based on them — and : The struct describes a piece of memory we want to allocate — its size in bytes and alignment: Memory alignment Alignment restricts where a piece of data can start in memory. The memory address for the data has to be a multiple of a certain number, which is always a power of 2. Alignment depends on the type of data: CPUs are designed to read "aligned" memory efficiently. For example, if you read a 4-byte integer starting at address 0x03 (which is unaligned), the CPU has to do two memory reads — one for the first byte and another for the other three bytes — and then combine them. But if the integer starts at address 0x04 (which is aligned), the CPU can read all four bytes at once. Aligned memory is also needed for vectorized CPU operations (SIMD), where one processor instruction handles a group of values at once instead of just one. The compiler knows the size and alignment for each type, so we can use the constructor or helper functions to create a valid layout: Don't be surprised that a takes up 32 bytes. In Rust, the type can grow, so it stores a data pointer, a length, and a capacity (3 × 8 = 24 bytes). There's also 1 byte for the boolean and 7 bytes of padding (because of 8-byte alignment), making a total of 32 bytes. is the default memory allocator provided by the operating system. The exact implementation depends on the platform . It implements the trait and is used as the global allocator by default, but the documentation does not guarantee this (remember the "unspecified" note?). If you want to explicitly set as the global allocator, you can use the attribute: You can also set a custom allocator as global, like in this example: To use the global allocator directly, call the and functions: In practice, people rarely use or directly. Instead, they work with types like , or that handle allocation for them: The allocator doesn't abort if it can't allocate memory; instead, it returns (which is exactly what recommends): The documentation recommends using the function to signal out-of-memory errors. It immediately aborts the process, or panics if the binary isn't linked to the standard library. Unlike the low-level function, types like or call if allocation fails, so the program usually aborts if it runs out of memory: Allocator API • Memory allocation APIs Memory management in Zig is explicit. There is no default global allocator, and any function that needs to allocate memory accepts an allocator as a separate parameter. This makes the code a bit more verbose, but it matches Zig's goal of giving programmers as much control and transparency as possible. An allocator in Zig is a struct with an opaque self-pointer and a method table with four methods: Unlike Rust's allocator methods, which take a raw pointer and a size as arguments, Zig's allocator methods take a slice of bytes ( ) — a type that combines both a pointer and a length. Another interesting difference is the optional parameter, which is the first return address in the allocation call stack. Some allocators, like the , use it to keep track of which function requested memory. This helps with debugging issues related to memory allocation. Just like in Rust, allocator methods don't return errors. Instead, and return if they fail. Zig also provides type-safe wrappers that you can use instead of calling the allocator methods directly: Unlike the allocator methods, these allocation functions return an error if they fail. If a function or method allocates memory, it expects the developer to provide an allocator instance: Zig's standard library includes several built-in allocators in the namespace. asks the operating system for entire pages of memory, each allocation is a syscall: allocates memory into a fixed buffer and doesn't make any heap allocations: wraps a child allocator and allows you to allocate many times and only free once: The call frees all memory. Individual calls are no-ops. (aka ) is a safe allocator that can prevent double-free, use-after-free and can detect leaks: is a general-purpose thread-safe allocator designed for maximum performance on multithreaded machines: is a wrapper around the libc allocator: Zig doesn't panic or abort when it can't allocate memory. An allocation failure is just a regular error that you're expected to handle: Allocators • std.mem.Allocator • std.heap Odin supports explicit allocators, but, unlike Zig, it's not the only option. In Odin, every scope has an implicit variable that provides a default allocator: If you don't pass an allocator to a function, it uses the one currently set in the context. An allocator in Odin is a struct with an opaque self-pointer and a single function pointer: Unlike other languages, Odin's allocator uses a single procedure for all allocation tasks. The specific action — like allocating, resizing, or freeing memory — is decided by the parameter. The allocation procedure returns the allocated memory (for and operations) and an error ( on success). Odin provides low-level wrapper functions in the package that call the allocator procedure using a specific mode: There are also type-safe builtins like / (for a single object) and / (for multiple objects) that you can use instead of the low-level interface: By default, all builtins use the context allocator, but you can pass a custom allocator as an optional parameter: To use a different allocator for a specific block of code, you can reassign it in the context: Odin's provides two different allocators: When using the temp allocator, you only need a single call to clear all the allocated memory. Odin's standard library includes several allocators, found in the and packages. The procedure returns a general-purpose allocator: uses a single backing buffer for allocations, allowing you to allocate many times and only free once: detects leaks and invalid memory access, similar to in Zig: There are also others, such as or . Like Zig, Odin doesn't panic or abort when it can't allocate memory. Instead, it returns an error code as the second return value: Allocators • base:runtime • core:mem Like Zig and Odin, C3 supports explicit allocators. Like Odin, C3 provides two default allocators: heap and temp. An allocator in C3 is a interface with an additional option of zeroing or not zeroing the allocated memory: Unlike Zig and Odin, the and methods don't take the (old) size as a parameter — neither directly like Odin nor through a slice like Zig. This makes it a bit harder to create custom allocators because the allocator has to keep track of the size along with the allocated memory. On the other hand, this approach makes C interop easier (if you use the default C3 allocator): data allocated in C can be freed in C3 without needing to pass the size parameter from the C code. Like in Odin, allocator methods return an error if they fail. C3 provides low-level wrapper macros in the module that call allocator methods: These either return an error (the -suffix macros) or abort if they fail. There are also functions and macros with similar names in the module that use the global allocator instance: If a function or method allocates memory, it often expects the developer to provide an allocator instance: C3 provides two thread-local allocator instances: There are functions and macros in the module that use the temporary allocator: To macro releases all temporary allocations when leaving the scope: Some types, like or , use the temp allocator by default if they are not initialized: C3's standard library includes several built-in allocators, found in the module. is a wrapper around libc's malloc/free: uses a single backing buffer for allocations, allowing you to allocate many times and only free once: detects leaks and invalid memory access: There are also others, such as or . Like Zig and Odin, C3 can return an error in case of allocation failure: C3 can also abort in case of allocation failure: Since the functions and macros in the module use instead of , it looks like aborting on failure is the preferred approach. Memory Handling • core::mem::alocator • core::mem Unlike other languages, Hare doesn't support explicit allocators. The standard library has multiple allocator implementations, but only one of them is used at runtime. Hare's compiler expects the runtime to provide and implementations: The programmer isn't supposed to access them directly (although it's possible by importing and calling or ). Instead, Hare uses them to provide higher-level allocation helpers. Hare offers two high-level allocation helpers that use the global allocator internally: and . can allocate individual objects. It takes a value, not a type: can also allocate slices if you provide a second parameter (the number of items): works correctly with both pointers to single objects (like ) and slices (like ). Hare's standard library has three built-in memory allocators: The allocator that's actually used is selected at compile time. Like other languages, Hare returns an error in case of allocation failure: You can abort on error with : Or propagate the error with : Dynamic memory allocation • malloc.ha Many C programs use the standard libc allocator, or at most, let you swap it out for another one using macros: Or using a simple setter: While this might work for switching the libc allocator to jemalloc or mimalloc, it's not very flexible. For example, trying to implement an arena allocator with this kind of API is almost impossible. Now that we've seen the modern allocator design in Zig, Odin, and C3 — let's try building something similar in C. There are a lot of small choices to make, and I'm going with what I personally prefer. I'm not saying this is the only way to design an allocator — it's just one way out of many. Our allocator should return an error instead of if it fails, so we'll need an error enum: The allocation function needs to return either a tagged union (value | error) or a tuple (value, error). Since C doesn't have these built in, let's use a custom tuple type: The next step is the allocator interface. I think Odin's approach of using a single function makes the implementation more complicated than it needs to be, so let's create separate methods like Zig does: This approach to interface design is explained in detail in a separate post: Interfaces in C . Zig uses byte slices ( ) instead of raw memory pointers. We could make our own byte slice type, but I don't see any real advantage to doing that in C — it would just mean more type casting. So let's keep it simple and stick with like our ancestors did. Now let's create generic and wrappers: I'm taking for granted here to keep things simple. A more robust implementation should properly check if it is available or pass the type to directly. We can even create a separate pair of helpers for collections: We could use some macro tricks to make and work for both a single object and a collection. But let's not do that — I prefer to avoid heavy-magic macros in this post. As for the custom allocators, let's start with a libc wrapper. It's not particularly interesting, since it ignores most of the parameters, but still: Usage example: Now let's use that field to implement an arena allocator backed by a fixed-size buffer: Usage example: As shown in the examples above, the allocation method returns an error if something goes wrong. While checking for errors might not be as convenient as it is in Zig or Odin, it's still pretty straightforward: Here's an informal table comparing allocation APIs in the languages we've discussed: In Zig, you always have to specify the allocator. In Odin, passing an allocator is optional. In C3, some functions require you to pass an allocator, while others just use the global one. In Hare, there's a single global allocator. As we've seen, there's nothing magical about the allocators used in modern languages. While they're definitely more ergonomic and safe than C, there's nothing stopping us from using the same techniques in plain C. on Unix platforms; on Windows; : alignment = 1. Can start at any address (0, 1, 2, 3...). : alignment = 4. Must start at addresses divisible by 4 (0, 4, 8, 12...). : alignment = 8. Must start at addresses divisible by 8 (0, 8, 16...). is for general-purpose allocations. It uses the operating system's heap allocator. is for short-lived allocations. It uses a scratch allocator (a kind of growing arena). is for general-purpose allocations. It uses a operating system's heap allocator (typically a libc wrapper). is for short-lived allocations. It uses an arena allocator. The default allocator is based on the algorithm from the Verified sequential malloc/free paper. The libc allocator uses the operating system's malloc and free functions from libc. The debug allocator uses a simple mmap-based method for memory allocation.

0 views
neilzone 2 weeks ago

Injecting deno into yt-dlp venv via pipx

gave me an error message of WARNING: [youtube] No supported JavaScript runtime could be found. Only deno is enabled by default; to use another runtime add –js-runtimes RUNTIME[:PATH] to your command/config. YouTube extraction without a JS runtime has been deprecated, and some formats may be missing. See https://github.com/yt-dlp/yt-dlp/wiki/EJS for details on installing one This baffled me for a bit, as it suggested that there was a runtime enabled by default (deno), and yet there was no supported JavaScript runtime. I have installed via (so ). I did not know about the “inject” command: And bingo. I now have an installed, supported, JavaScript runtime.

0 views
baby steps 3 weeks ago

Hello, Dada!

Following on my Fun with Dada post, this post is going to start teaching Dada. I’m going to keep each post short – basically just what I can write while having my morning coffee. 1 Here is a very first Dada program I think all of you will be able to guess what it does. Still, there is something worth noting even in this simple program: “You have the right to write code. If you don’t write a function explicitly, one will be provided for you.” Early on I made the change to let users omit the function and I was surprised by what a difference it made in how light the language felt. Easy change, easy win. Here is another Dada program Unsurprisingly, this program does the same thing as the last one. “Convenient is the default.” Strings support interpolation (i.e., ) by default. In fact, that’s not all they support, you can also break them across lines very conveniently. This program does the same thing as the others we’ve seen: When you have a immediately followed by a newline, the leading and trailing newline are stripped, along with the “whitespace prefix” from the subsequent lines. Internal newlines are kept, so something like this: would print Of course you could also annotate the type of the variable explicitly: You will find that it is . This in and of itself is not notable, unless you are accustomed to Rust, where the type would be . This is of course a perennial stumbling block for new Rust users, but more than that, I find it to be a big annoyance – I hate that I have to write or everywhere that I mix constant strings with strings that are constructed. Similar to most modern languages, strings in Dada are immutable. So you can create them and copy them around: OK, we really just scratched the surface here! This is just the “friendly veneer” of Dada, which looks and feels like a million other languages. Next time I’ll start getting into the permission system and mutation, where things get a bit more interesting. My habit is to wake around 5am and spend the first hour of the day doing “fun side projects”. But for the last N months I’ve actually been doing Rust stuff, like symposium.dev and preparing the 2026 Rust Project Goals . Both of these are super engaging, but all Rust and no play makes Niko a dull boy. Also a grouchy boy.  ↩︎ My habit is to wake around 5am and spend the first hour of the day doing “fun side projects”. But for the last N months I’ve actually been doing Rust stuff, like symposium.dev and preparing the 2026 Rust Project Goals . Both of these are super engaging, but all Rust and no play makes Niko a dull boy. Also a grouchy boy.  ↩︎

0 views
The Tymscar Blog 3 weeks ago

How To Quiet A Ugreen 4800 Plus Without Sacrificing Drive Temps

I recently got a Ugreen 4800 Plus NAS, and it is basically perfect for what I wanted. Four bays, enough CPU, enough RAM, nice build quality, and it does not look like a sci-fi router from 2012. The first thing I did was wipe the OS it shipped with and install TrueNAS. That part was also great. The not so great part was the noise. I expected it to be louder than my old Synology, mostly because I moved from “HDDs in a plastic box” to “a more PC-like NAS with more airflow”. Still, it was louder than I thought it would be, and it had this annoying behavior where the fan would randomly ramp up. Which is exactly the kind of thing you notice at night.

0 views