This article describes my 2017 port of a particular Yoga revision. It is a record of that work, not a claim that the port implements today’s Yoga API or passes its current test suite.
I was working on desktop GUI apps for Windows in Go.
The apps used native Win32 controls, so I did not have to implement buttons, list boxes, and other controls.
However, Win32 doesn’t have anything to help you lay things out on the screen. Manual positioning is painful.
I started looking into writing a layout engine. There are many systems to be inspired by.
I had done some work in WinForms so I could copy their ideas.
I could look into copying Android’s layout logic.
I could look into implementing constraint-based layout like in iOS.
The option I liked best was CSS flexbox. It’s not the simplest, it’s not the most complicated, but the biggest win is that many people, including myself, are already familiar with it.
I started looking for an implementation in Go. I found one in the
shiny project, but it’s a simplified implementation.
The implementation I chose was Facebook’s
Yoga, which at the time was written in C, with bindings to several languages.
I could write cgo bindings but I prefer my Go pure so I decided to manually port the C code to Go, all 4.5k lines of it.
The result is the
flex package.
Here’s how the porting process went.
Phase zero - picking a name for the package
It was tempting to pick a name that would be some combination of yoga and go e.g. goyoga or yoga-go.
That would be a bad idea. Including go in a package or repository name is bad (but unfortunately happens too often).
My rules for a good name for a Go library are:
- it’s short
- it’s descriptive
- package name matches repository name
I could use yoga, to better highlight the connection with the original project, but it’s not descriptive.
I could use
flexbox to better highlight the connection with
CSS flexbox, but it’s a bit long.
Phase one - getting the Go port to compile
You can’t port 4.5k lines of code in one sitting so it was important to have a strategy that allows for making incremental progress.
The important part was keeping track of porting progress. For that I cloned Yoga’s repository. After converting a piece of C code to Go I would delete it from my fork. That way I would keep track of code that still needs to be ported.
It’s important to get an early boost so I started with
the simplest parts: structures, enums.
The porting process was boring but relatively uneventful.
Some porting comments:
- Go and C reverse the order of type and variable names in declarations. Reversing it manually is boring and error prone. There were many repeated declarations that I could do with a simple search-and-replace, e.g.
YGNodeRef node => node *YGNode
- search-and-replace
-> to . as Go uses . for both cases (something that C++-28 should adopt)
- Go doesn’t support a ternary operator and Yoga’s developers are infatuated with it. That was one part where I had to be extra careful as it was more than a mechanical change
- Go does not support bitwise
^ and | on booleans. For boolean values, XOR can be written as x != y, and a = a | b as a = a || b. Remember that || short-circuits; preserve any required side effects when translating expressions.
- a
switch statement needs attention because a C case falls through by default
- Yoga uses
float for coordinates and uses a few functions like fmaxf etc. The math package mainly operates on float64. Go 1.21 added built-in min and max, including support for float32, but their NaN behavior differs from C’s fminf and fmaxf. That distinction matters here; see the Go specification.
- Yoga code was relatively easy to port partly because of the lack of string handling, which is where Go and C differ a lot
It took me 2 days to port the C code to Go, get it compiling and passing some tests. But some tests were failing.
Phase two - fixing bugs
Yoga had many tests, which gave me confidence that porting them would help verify the Go implementation.
After porting the code, I started porting the tests, and they were failing.
I was in a pickle.
Two days were enough to mechanically port the code but not even close enough to understand it.
At the time, I converted a failing test into a standalone program to debug it. Today,
Delve supports
dlv test, and the
Go extension for VS Code can debug individual tests directly.
I was pleasantly surprised that the debugger in Visual Studio Code worked well.
I found one porting mistake by stepping through the code but fixing it didn’t fix test failures.
I spent a bit more time stepping through the code but not knowing what results to expect I decided that it was not a promising approach.
I decided to re-check every line of code.
Initially I ported the code in random order, so while rechecking it I also rearranged the code to match the order of the C code. That will help when porting Yoga improvements in the future.
It was predictably boring. It took another day, and I found two more porting bugs.
The tests were still failing and I was a bit stuck.
I did another pass in the debugger and fortunately inspiration struck.
Yoga used NaN to represent undefined. I noticed that the fmaxf function that I wrote returned NaN when any argument was NaN.
I compared my fmaxf implementation with the C implementation, and it turns out that if one of the arguments is NaN but the other one isn’t, it should return a non-NaN value.
It was easy to fix and that fixed all the tests.
Phase three - Go-ifying the API
The code was working but it was far from idiomatic Go code.
The next phase was tweaking the API to be more Go-like.
The majority of changes were:
- making struct fields public
- removing the
YG prefix as packages obviate the need for that
- renaming accessor functions to methods (from
func NodeFoo(node *Node) => func (node *Node) Foo())
It’s a mechanical, boring process.
Some transformations could be done with regex search & replace. The rest required manual edits.
Having lots of tests really helped in making sure that the code is still correct.
The status
The port was finished and passed the tests from the Yoga revision I ported.
The API is still a bit awkward by Go standards. Too many methods, not enough direct access to variables.
Partly it’s because I wanted to stay close to C code so that future changes to Yoga can be ported easily.
Partly it’s because of implementation choices and the nature of the problem.
There are many flexbox properties, but only some are specified for a given element. The rest take default values, which might be undefined.
How to represent that in Go? Ideally, the zero value of the type would represent the default value so that a style definition can be constructed as &Style{} and then we can set the properties that are non-default.
Unfortunately, that doesn’t work well for properties that are numbers because zero might be a valid value.
Yoga uses float32 for numbers and NaN for the undefined value of a CSS property.
Another option is used in the database/sql package for types like sql.NullString that need to indicate nullability. They are represented as a struct that combines a value and a bool field, Valid. The zero value of a bool is false, so by default values start as invalid (undefined).
That makes setting and reading values more explicit. Since Go 1.18, an application can express this pattern once as a generic
Optional[T] struct. Go 1.22 also added
sql.Null[T] for nullable database values; a UI model need not depend on
database/sql to use the same idea.
At the end of the day it’s better to have solid, working code with awkward API than not having the code at all. A full flexbox implementation is not trivial to write.
The resulting
flex package met my needs at the time. Check its supported behavior against your requirements before using it for a new layout engine.
Notes on automatic translation
Go is so close to C. Wouldn’t it be great if there was a program that could take C code and turn it into Go code?
There were a few attempts to do that:
I did not use these tools for the 2017 port. Their capabilities have changed since then, so this list is historical rather than a current comparison. For a new translation, test the tool against the actual C code and validate the result against the original implementation.