How to create a GUI app using libui-ng on Windows?

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

изображение

3 Likes

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

  • On Linux and MacOS, you specify the flags for the GNU ld linker.
  • On Windows, on the other hand, you specify the options for MSVC linker.

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

1 Like

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.

1 Like

This should be equivalent to @[Link("gtk+-3.0")]

1 Like

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 :weary:

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 %}
3 Likes

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.

2 Likes

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:

  • Run libui’s main loop on a single thread inside an Isolated context.
  • Execute background tasks in a MultiThreaded context to distribute CPU-intensive work.
2 Likes