Go Plugins Link to heading
A Walk Through History Link to heading
I’ve been writing go for a long time. Since early 2012 or so. I’ve used all
kinds of side projects (most of them open source) to learn new things. I
contributed to gorp and wrote
gorq (originally gorp-queries) to learn
the laws of reflection. At Pivotal, I used some of my free time to study the
atomic package to reduce lock contention in what eventually became our diodes
package. I wrote
hel to learn the parser and ast packages.
The tl;dr is that I like to use projects to teach me new things.
In 2015, Ben Clayton released a new project
called gxui. I had been itching for a UI
library in go since I started writing it - I thought it would be so cool to
write desktop applications in go. With CPU core counts getting more and more
varied, I thought that goroutines and the concurrent design they enabled could
work well in a desktop setting - and I wanted to experiment. So as soon as I saw
gxui, I started a project to mess around with it - named gxui_playground at
first. Huge props to Ben - despite some issues I have with the API, none of the
rest of this post happens without gxui.
Fast forward a few months and I’ve put together a text editor. It wasn’t
great, but it did edit text. And what’s more, it drove me to go more in depth
on other packages - parser and ast for syntax highlighting, godef for
go-to-definition, later gopls and the language server protocol… Plus, I was
actually using the editor to edit itself. It was so fun.

And since it was a project I was using, it needed a new name - “gxui_playground”
no longer described it very well. When I can’t think of something descriptive to
name a project (like protoql), I just pick a
name from mythology - I would rather have a project with a bad name than never
release the project. So I looked through the gods of Norse mythology and settled
on “Vidar” - the Norse god of patience, silence, and revenge. It seemed
fitting - one of my biggest complaints about modern editors is how often they
get in the way of reading code, because they think that I paused typing
because I need code completions or something. I would much rather ask my editor
for completions - the vast majority of pauses in my typing are because I’m
re-reading my code to make sure I’m not doing something dumb. So “Vidar” made
sense - I wanted an editor that was patient and silent.
I’m not really sure where “revenge” comes in, but 2 out of 3 ain’t bad.
Then along came go 1.8 with a shiny new plugin package. Now, I’d known it was
coming for a little while by that point, so I had already prepped parts of my
codebase
to use it. Then I updated vidar so that on go 1.8+ on linux, plugins build as
standalone .so files and vidar loads them with the plugin package. See this
commit.
A Logo Link to heading

At some point in this mix, I sat down and made a logo. I dug into some lore about Vidar first - the tl;dr is that he takes leather scraps from cobblers to build up his boot because he’s destined to step on Fenrir’s (the wolf that eats Odin) lower jaw and plunge his sword into Fenrir’s skull.
I probably could have done something with the boot, but nothing memorable was coming to mind. In the end, I chose a kind of three-pronged crown silhouette, with Fenrir’s ears as the outer prongs and Vidar’s sword as the middle prong, plunging through Fenrir’s head from inside his mouth.
This doesn’t really have anything to do with the plugin package, but I’m proud
of it. I wanted to share.
Issues With Plugins Link to heading
I don’t think I was the first to run into issues with plugins. I mean, I tried
them out pretty early, but I did wait until they were in an official release.
I’m sure there are plenty of people who were trying them on go tip before
1.8 was out. And everyone who uses plugin runs into issues - you’ll find
sentiment all over the internet that the plugin package is best avoided.
To summarize the problems really quickly:
- Plugins must use the exact same version of every shared dependency.
- “shared” in this case means any dependency shared with the parent project (the one loading the plugin) or any other plugin.
- The go build system embeds file paths in the build process, and those file
paths are considered part of the “version” of the file.
- This means that even the exact same code built on the exact same computer
could trigger plugin conflicts if you built it with a different
GOPATH. - They added the
go build -trimpathoption to help solve this.
- This means that even the exact same code built on the exact same computer
could trigger plugin conflicts if you built it with a different
go build -buildmode pluginonly worked on linux.- They would implement support for darwin (OS X) a few versions later.
Windowswould take much longer. More on that later.
The version has to be exactly the same. And at the time, there was no such
thing as “go modules” - we were still developing inside of a $GOPATH and had
no easy go.mod to tell us which versions we depend on. If someone merged a
commit to main in github.com/my/awsome/logging/package, that was now what we
depended on.
Even worse, work-in-progress changes that version too. Let’s say that Vidar
defines a type, git.sr.ht/~nelsam/vidar/input.Action, and allows plugins to
implement a top level func Actions() []input.Action. Now let’s say that I’m
trying to debug something in Vidar, so I add some debug logs to an unrelated
package. When I rebuild Vidar with the WIP debug logs, the go runtime considers
it a different version than the version that my plugins were built with. And
since I’m not planning to publish this version, I have to add the plugin to
the $GOPATH I’m developing Vidar in to build it against Vidar’s current code.
The go compiler also embeds file paths into the compiled code for debug
purposes - e.g. so that stack traces can print the file name and line number.
I’m not sure what the technical reason is for this, but the go runtime considers
that file path to be part of the version of a file - maybe because it has to
print stack traces? Those file paths are probably built in to the panic handling
logic in the compiled code. Anyway, at the time each of my projects had their
own GOPATH - and that caused even more issues. I wrote a Makefile target
that built the editor and the plugins inside of a container so that all of the
versions and paths were always the same. But I still ran into issues often - it
was brittle at best.
These issues were frustrating and a lot of people gave up on plugin pretty
quickly. But I’m stubborn.
The Workarounds Link to heading
The thing is … once you got a plugin to load, it was great. Not only do you get the same performance as calling native code, but you also get all the power of the go language.
As a quick review: in go, you implement interface types implicitly. There is
no implements keyword or anything of the sort. In my experience, this is a
pretty dramatic change - you can kind of use interface types as duck types.
There is a lot of power in decoupling the importing package and the implementing
package so that they know nothing about each other. I’ll write another post in
the future with more detail on this, but those details aren’t really important
here.
The important thing is that you can implement an interface without ever importing the package that contains the interface.
You can implement an interface … Without importing the package.
The majority of the issues with plugin are around shared imports. And really,
the biggest issue with that is when the plugin imports some sub-package of the
parent. So … what if both vidar and the plugins just … didn’t import each
other? Then we only have to worry about libraries that we both import.
I made a series of updates to vidar and the plugin that implements go language support (syntax highlighting, go-to-definition, etc). At the end, the go language plugin imported exactly one shared package (defining parameter and result types), and I could safely update (and debug!) vidar without rebuilding the plugin.
The file path issue is largely solved by -trimpath, so I didn’t need to do
much with that.
And by the time I’d solved most of these issues, the plugin package supported
darwin - so I had a few friends who had mac computers give it a shot. There
were some issues unrelated to plugins - especially around watching files for
changes. But the plugin system worked great.
Windows Link to heading
My main contributor was on Windows. I didn’t want to release anything that
didn’t work for them. And there was no real word on when (or even if) plugin
would have Windows support. I subscribed to the github
issue and continued to develop
vidar, but whenever I implemented a plugin I made sure the Windows version
imported the plugin directly.
But there was something that had been itching at the back of my brain for a
while, at this point. You see, google abandoned gxui not long after I started
using it. The author ran out of free time and there wasn’t really anyone else to
take on maintenance. I forked it and fixed any issues I ran into, but I also
didn’t have much free time - and there were new UI frameworks coming out all the
time. So I kind of wanted to try another UI library. But my code was tightly
coupled to the gxui mixins.
I had just put in all that effort to decouple plugins from vidar, and when I looked at my tightly coupled gxui code … I didn’t think there was a good reason for the tight coupling. Couldn’t I just … move the UI drawing code to local interface types and then … load UI libraries using plugins? I mean, I had no idea which new UI library I wanted to replace gxui with and I’d always wanted to support a CLI interface - supporting drawing libraries as a plugin would let me experiment.
Besides, this was a big task - effectively a full rewrite of the editor from the ground up. Surely Windows support would be done by the time I finish that. Right?
The UI Plugin Link to heading
Vidar uses specific types of UI elements, and it needs to be able to create them on the fly. When a user opens a file, it needs to be able to create a new text editor to contain the text, show carets, etc. But as long as those types are capable of the methods I need, it didn’t matter how they performed the drawing.
Step 1 was obvious: Use interface types to decouple the implementation from what vidar actually needed, then remove the embedded UI types. Vidar could track the associated UI elements, but after decoupling the code there was no good reason for vidar’s code to embed the UI elements. Plus, embedding types messes with readability - it’s too hard to know which methods a type is capable of if you have to look in several files (and in this case, several projects) to find them.
But just because it was obvious didn’t mean it was easy. This took a long time.
Between my project’s heavy coupling, the fact that the plugin package still
didn’t support Windows, and some burnout I was running into … nothing was
happening very fast. I fell back to drawing boxes and lines, thinking through
the project, what it needed to do. And I also decided that as long as I’m
changing so much, it was probably time to clean up all the old tech debt.
Remember, I started the project as gxui_playground. I never expected to build
an editor that anyone (even me) used on any kind of a regular basis.
Minor aside: handling text in editors is a really interesting subject! I was
bouncing back and forth between ropes and gap buffer implementations until I
came across the piece table pattern. Take a look at my
implementation
if you’re curious.
At long last, in 2020, I got a build working. This had the full rewrite - from cleaning up all of the internal logic and decoupling the UI, to including more robust tests. I still needed to port a lot of features from the old build over, but it was a good start.
And surely, Windows support would be soon right? I could keep this version on a branch until then.
Windows. Again. Link to heading
By 2023, I was starting to think that maybe the plugin package would never
support Windows. By then, I had moved off of github and on to sourcehut, so
I thought I might as well merge all of the work I’d been doing to main. It
wasn’t changing the github project so ideally the old code would still work. So
I merged, cut a release, and then started trying to figure out what to do about
plugins on Windows.
You can look at the sourcehut TODO for all of the details, but a quick summary of things I tried:
goloader. Long story short: plugins have strict limits on what they can import. Vidar had to declare more and more packages to share with plugins, and it didn’t feel like there was an end.syscall.LoadLibrary: copying my comment from the sourcehutTODO:I spiked on option 1, since I thought it would be the quicker one to eliminate. I just tried to compile the golang plugin as c-shared and then check that it could be loaded at all. Turns out that c-shared exports only support simple types - no structs, no methods. It’s not really surprising, but that means it’s a non-starter.
WSL: WSL 1 did not support wayland or X11 programs. It looks like WSL 2.0 must have been out when I looked at this, but I missed it or couldn’t get it to work or something. Either way, I didn’t think it was an option.yaegi: This one almost worked, but vidar only knows about some of the methods that actions are capable of. Other actions, key bindings, etc can type-assert those actions to get access to methods that vidar isn’t aware of.yaegiwraps up interface types in concrete types, losing access to any methods that aren’t part of the interface type.- I tried forking
yaegito solve the issues I was running into and felt like it was possible to fix. But then I started running into parsing issues, particularly around generics. Maybe I could have solved it given enough time, but I thought there had to be a better way. Besides, I was already low on free time, I didn’t need yet another fork to maintain. hashicorp/go-plugin. Really neat idea, and it would both add flexibility (it would be trivial to write plugins in languages other thango) and solve the import compatibility issues. But there’s an overhead to using it, and invidarthere are some ways for plugins to insert logic between a user pressing a key and seeing a character show up on the screen. I hate editors that feel laggy. Thego-pluginpackage also doesn’t directly let me type assert - I would need custom code to “upgrade” thegrpcconnection with extra methods.
At long last, I realized that WSL2 supports wayland and X11 windows - so I tried that. It seemed to be working okay in a Windows VM.
And then I got an email from github. Someone had made a new comment on the github issue tracking Windows support for plugins.
I’d been getting emails about this issue for a while, now. That’s how I knew to
try goloader. But most of the comments were questions like, “what are my
options?” and “is anyone working on this?”. The last time I got an email on the
subject, it started with “Plugins have not been a priority on any platform. They
barely work anywhere.”
This comment was different.
Hey everyone, I’ve managed to get part of the plugin architecture running on Windows. Unfortunately, there are still a few features that aren’t working, such as allocations, fmt, sync.Pool, and goroutines.
For “fmt,” only “Print” and “Log” are working. That took me about three days of work. I’ll release the Go project once everything is working properly. I still need to test a lot of Go functions. Here are a few screenshots.
Huge props to zandercodes. I had been waiting for this comment for almost a decade.
Gio Link to heading
Alright, so no more fighting with a platform that I don’t use. People can use WSL2 for now, and maybe get official plugin support in a future version. Great!
I can finally get back to work on the editor itself!
In the sidelines, I had been gradually implementing the UI interface types with
gio in a plugin. I had the plugin loading, but there was
a lot of stuff still not working. I just didn’t feel very motivated to finish it
if I couldn’t find a way to load plugins on Windows. But now that it was working?
You can see the commit history here. Over the course of about 10 days (2026-07-10 through 2026-07-20), I finished the text editor (including scrolling), linear layouts, tabbed layouts, and colored spans in text. It was shaping up really well - and it even built in about two seconds on my system.
Vidar, though, still had gxui built in. That was a problem. You see, gxui
wrapped up calls to glfw (a C library) using CGO, and building gxui could
take minutes. I didn’t want someone who was using the new gio UI (which builds
in single digit seconds) to spend minutes building gxui. Besides, eliminating
vidar’s dependency on my gxui fork felt great after all these years.
So I also introduced
vidar-plugin-gxui. Easy
enough - I just took the gxui code out of vidar and put it in its own package.
Great!
Except … now I’m getting plugin load errors.
plugin was built with a different version of package […]
Ugh. Do you see what I forgot?
Plugins must use the exact same version of every shared dependency […] with the parent project […] or any other plugin.
🤦
[…] or any other plugin.
Yep. vidar-plugin-gio and vidar-plugin-gxui both loaded the same logging
library, and they were at different versions. Since I had both plugins
installed, it would load one first and then fail to load the other.
Really solving plugin load problems. For real. Link to heading
For a long time, I had planned on building some way for users to download
plugins either in their config file or through the editor itself. It wasn’t a
priority, but it was on the list. But if plugins are always go modules, and if
go get resolves dependency conflicts, couldn’t I just … do that in the
editor? It kind of breaks the concept of reproducible builds, but … honestly,
that’s not likely to work with plugins, anyway.
Okay, let’s give it a quick spike, see what happens.
First step: use //go:embed go.mod go.sum to embed vidar’s go.mod and go.sum
files. Cool, that’s an easy step.
Second step: update the user configuration so they can tell vidar which plugins they want, including the go module. We can support closed source build systems later.
Minor complication: we required some weird stuff that
github.com/BurntSushi/toml doesn’t support right now. So toss together a toml
parser/lexer
pair and get
it working for our config files. Maybe we can contribute something upstream
later.
Third step: when vidar can’t find a plugin or the load error contains “plugin was built with a different version”, start a rebuild/reload process:
- Create a temp directory.
- Write our
go.modandgo.sumto that directory. - For each plugin in the user’s config file, from the temp directory:
go get -tool {plugin-module}go build -buildmode plugin -o {plugin-dir}/{plugin-name}.so {plugin-module}
- Restart Vidar. On linux, this means
syscall.Exec; on windows, just call vidar withexec.Cmd.
The magic is step [2]. Since we’re doing all of this from a go module, go itself syncs our dependencies and the dependencies of our plugins. As long as we build all of the plugins from that module, go guarantees the same versions.
There’s a little more in there, namely around making sure we have a UI first so
that we can give the user feedback. We also don’t want to rebuild Vidar itself -
we intend to track changes to the go.mod and go.sum as we rebuild each
plugin so that we can tell the user about plugins that aren’t compatible with
the installed version of Vidar.
And finally, we need some default plugins. Obviously we need a UI plugin - gio
is working so we might as well default to that. We’ll document how to switch to
the older (maybe more stable?) gxui plugin in the config itself, but I’d
rather get bug reports about gio than gxui at this point. And since Vidar is
written in go, it makes sense to include vidar-plugin-go in the defaults as
well.
The End Link to heading
At long last, you can run the following:
$ go run git.sr.ht/~nelsam/vidar@v0.8.0
It will download, run, realize that the default plugins aren’t installed,
install the plugins, and start up. And if you want to switch from the gio UI
to the gxui UI, all you have to do is make a config file:
# on linux, this is ~/.config/vidar/conf.d/gxui.conf
[plugin]
gio = "-"
gxui.module = "git.sr.ht/~nelsam/vidar-plugin-gxui"
[ui]
plugin = "gxui"
A simple config change, and then everything draws to the screen with a whole different library:
I’m really happy. I wouldn’t recommend the plugin package to most people, but
I’ve tried a lot of other ways of dynamic code loading and for this exact use
case, plugin is the right combination of easy and fast.
And I still can’t get over the fact that I got the whole UI to load as a plugin.
It’s so cool. Now I can write vidar-plugin-termbox (or something) to get CLI
support working! Or even vidar-plugin-web to launch a web server!
Or maybe I should finish implementing all the features from the old version first. You know, like handling mouse input. That’s probably important.