well, the state of project is slightly lower than my usual bar for github projects, but i didn’t have much time lately so why not.
Uploaded it to GitHub - konovod/nappgui-cr: Crystal wrapper for NAppGUI
progress so far

well, the state of project is slightly lower than my usual bar for github projects, but i didn’t have much time lately so why not.
Uploaded it to GitHub - konovod/nappgui-cr: Crystal wrapper for NAppGUI

Thank you very much.
I enjoyed running lowlevel_example.cr and highlevel_example.cr !
What I really didn’t understand was which linker option to use for each OS. Especially on Windows.
Thanks to @konovod uploading his nappgui-cr project to GitHub, I now know how to write the link flag on Windows.
@[Link("#{__DIR__}/../../core")]
However, this does not work on Linux. Is there a good way to cross-platform?
https://github.com/crystal-lang/crystal/blob/master/src/compiler/crystal/codegen/link.cr
Looking at the source code of crystal above, I see the following
I do not know much about static languages, but my conclusion is simple. These two linkers are not compatible in their flags. So, it is easier to use macros to set different flags for different operating systems.
{% if flag?(:windows) %}
@[Link("#{__DIR__}/../../libui")]
{% else %}
@[Link(ldflags: "-L #{__DIR__}/../../ -lui")]
{% end %}
lib LibUI
This works, but there may be a better way to write it.
Distribution of shared libraries remains a challenge. In the case of Ruby, I wrote a Rake task to download so, dlls, dylib to the vendor directory. In the case of Crystal, postinstall mechanism is one option.
scripts:.
postinstall: crystal run download.cr
but I am not sure if this is really a good idea. Technically, the shared library is a binary file, so it should be in a generic location like /usr/lib, not ./lib/libui-ng/ in the project. But libui-ng is a portable library for hobbyists, and should work immediately after shards install if possible.
If you do not want the terminal screen to pop up when app.exe starts, add
--link-flags=/SUBSYSTEM:WINDOWS.
Thanks again HertzDevil and konovod.
Finally, I was able to run libui-ng on Windows and other platforms in the same way.
Passing a Proc to a C function is difficult, but I think I was able to implement according to the API reference. This is the first time I used Box.
Link to Prototype
I now know the cross-platform compilation options for building an app with static libraries.
module UIng
{% if flag?(:windows) %}
@[Link("User32")]
@[Link("Gdi32")]
@[Link("Comctl32")]
@[Link("UxTheme")]
@[Link("Dwrite")]
@[Link("D2d1")]
@[Link("Windowscodecs.lib")]
@[Link("#{__DIR__}/../../libui")]
{% elsif flag?(:linux) %}
@[Link(ldflags: "`pkg-config gtk+-3.0 --libs`")]
@[Link(ldflags: "#{__DIR__}/../../libui.a")]
{% elsif flag?(:darwin) %}
@[Link(framework: "CoreGraphics")]
@[Link(framework: "AppKit")]
@[Link(ldflags: "#{__DIR__}/../../libui.a")]
{% end %}
Without AI’s assistance I would not have found this option. This is because static libraries can only guess the libraries they depend on from the function names in the error messages, and few people are equally familiar with Windows, Mac, or Linux. I have to thank AI for the advances it has made in recent years.
This should be equivalent to @[Link("gtk+-3.0")]
Thanks! It worked.
Crystal now supports the MinGW platform, so I’m trying to get libui working.
But, Windows is such a pain in the ass…
I respect Microsoft’s commitment to maintaining backward compatibility, but Windows is never something a hobbyist programmer can enjoy ![]()
This linker flag doesn’t always work as expected, but I’m putting it out here—someone might find it useful.
module UIng
{% if flag?(:msvc) %}
@[Link("User32")]
@[Link("Gdi32")]
@[Link("Comctl32")]
@[Link("UxTheme")]
@[Link("Dwrite")]
@[Link("D2d1")]
@[Link("Windowscodecs")]
@[Link("msvcrt")]
@[Link("#{__DIR__}/../../../libui")]
@[Link(ldflags: "/SUBSYSTEM:WINDOWS /MANIFEST /MANIFEST:EMBED /MANIFESTDEPENDENCY:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' language='*'\"")]
{% elsif flag?(:win32) && flag?(:gnu) %}
@[Link("stdc++")]
@[Link("supc++")]
@[Link("user32")]
@[Link("Gdi32")]
@[Link("Comctl32")]
@[Link("D2d1")]
@[Link("Dwrite")]
@[Link("WindowsCodecs")]
@[Link("Uuid")]
@[Link("Winmm")]
@[Link("Uxtheme")]
@[Link("ucrt")]
@[Link(ldflags: "-mwindows")]
@[Link(ldflags: "#{__DIR__}/../../../libui.a")]
@[Link(ldflags: "#{__DIR__}/../../../comctl32.res")]
{% elsif flag?(:linux) %}
@[Link("gtk+-3.0")]
@[Link("m")]
@[Link(ldflags: "#{__DIR__}/../../../libui.a")]
{% elsif flag?(:darwin) %}
@[Link(framework: "CoreGraphics")]
@[Link(framework: "AppKit")]
@[Link(ldflags: "#{__DIR__}/../../../libui.a")]
{% end %}
The most important libui problem is that it is difficult to have official libraries (whether static or shared libraries) that work in various Windows environments.
libui-ng has not released official binaries, and for good reason. Every time I attack this problem, I learn a little more about Windows, but at the same time I hate Windows a little more.
However, even in its current state, it is still possible to create a very simple GUI application for Windows, package it with Inno Setup, and distribute it. I will blog about that soon.
libui-ng doesn’t provide official binaries, so we have to build it ourselves. Inspired by a recent Crystal issue, I was able to get static linking to work with MSVC by adjusting the /MT and /MD compiler flags.
I’m not familiar with Windows development. But even with all the little traps of Windows, AI has been a huge help in making sense of it. A few months ago, I felt that AI still struggled with memory management. But now Claude is capable.
Thanks to all this, I can now build simple Windows GUI apps as a single executable file, with no additional DLLs. When packaged with Inno Setup, it really feels like the classic freeware apps from the early 2000s.
Creating app and dmg for macOS and deb with fpm for Ubuntu also worked well.
This sample was made with Vibe Coding
In order to use Area and Table safely in Uing, a few more improvements and modifications are needed to prevent memory leaks and double freeing (mainly by GC).
As for the major controls, I think they are already at a practical level.
I had trouble figuring out how to register a Crystal closure with a C API whose structs contain callback-function pointers as members.
The solution was to inherit the C base struct on the Crystal side and add an extra field that stores a boxed Proc pointer. This preserves the original ABI while letting Crystal invoke the callback safely.
I also recently discovered Fiber::ExecutionContext::Isolated. Using it, I should be able to structure an application as follows:
Unfortunately, on macOS, AppKit must be initialized on the main thread, so it is difficult to run UIng inside ExecutionContext::Isolated.
This means that even if we create a GUI with UIng on macOS, we cannot isolate it into ExecutionContext::Isolated. It works on Linux, though.
Because the native GUI on macOS is mostly based on AppKit, separating the GUI thread into an isolated execution context does not seem practical.
This is a bit disappointing.
Damn. That’s an edge case, but I should have thought of that ![]()
Apparently Android has the same kind of behavior: the UI should be managed from the main thread, and the rest of the application shall live in threads.
This isn’t possible right now, but maybe there could be a comptime flag to create a Parallel context (still the default context) and declare the main thread as being an Isolated context that (transparently) spawns into the default context? ![]()
We can’t do it automatically for Android and macOS targets as they’d behave differently from other targets (bad), and we don’t want to work like that on every target because it would affect current programs that expect the main fiber to run alongside other fibers (also bad).
Another approach would be to make the runtime and its setup more explicit in code - as long as the thread setup is explicit in it it would be straightforward to override it into something that fits the problem at hand.
As a bonus effect it would make it more discoverable - if you have something in your stacktraces where you can look and see exactly how things like fibers, gc and other default setup happens then it is easier to figure out how things work. Add in a couple of lines that handles the prelude and things will be really easy to find..