Bluenote was right. In case anybody finds this in the future: I opened an [issue](https://github.com/nim-lang/Nim/issues/5381) and the answer was "Use and read about setupForeignThreadGc." Once you call that function, the compiler tells you you _have_ to turn --tlsEmulation:off, and once you do that the code works.
I'm still curious about how this all works. I found a bit of info in [the docs](https://nim-lang.org/0.13.0/backends.html#memory-management-thread-coordination) which does say that you _must_ call setupForeignThreadGc or the GC may cause a crash. In my example it seems like this is true even if you aren't obviously using the GC, and it might cause a hang instead of a crash. In [the nimc docs](https://nim-lang.org/docs/nimc.html) I found tlsEmulation stands for [Thread Local Storage](https://en.wikipedia.org/wiki/Thread-local_storage) emulation, but that's all I know. Looks like something gcc does sometimes? What happens when it gets turned off, does that mean pthreads tls doesn't work? Would be interested from a learning-how-all-this-works perspective. Finally: it kind of sucks that the code needs to be different to work in threaded vs. non threaded modes. It's fair that if you want to interface with c, you have to be willing to understand a bit more of how the memory management sausage is made. But as a new user, it feels strange that the default mode of Nim is non-threaded, and that turning on thread support (with no other change) breaks my code that interfaces with c in unexpected, non-obvious ways.