Writing .exe programs for Linux
This page intends to document the different techniques I used in my Win32 programs to make them Linux friendly.
The need came after the two historical Linux programs: "exe-thumbnailer" and its successor "icoextract", stopped working on Ubuntu 24.x so I decided to write my own routines.
Table of Content
- Detecting if we are on a Unix system
- Extracting .exe icons
- Showing an .exe icon in Linux file explorer
- Showing an .exe icon in the Linux Dock
- Registering an .exe in the Linux App Menu
- Translating between Unix and Windows paths
- Mapping Wine drives to Unix paths
- Calling native Unix commands from a Win32 .exe
- Getting Unix command output from a Win32 .exe
- Creating Windows shortcuts on Linux
- Looking Glass debugger
- Hide HDD folders like "System Volume Information" or "RECYCLER"
Detecting if we are on a Unix system
The first of all things that our Win32 .exe programs need to do is to check and adapt themselves if we are on a Unix system running through Wine.
This is done by getting the procedure address - via the Win32 API GetProcAddress() - of the Wine-only function "wine_get_version()"
contained in the Windows dll "ntdll.dll", loaded through the Win32 API GetModuleHandle().
If that procedure exists (its address is not null) then we are on Unix, else we are on Windows.
The full code for the Unix detection can be found in unix.inc, in the IsOnUnix() function.
Extracting .exe icons
For a Windows .exe to show its icon in the Linux file explorer, Linux Dock, or Linux App Menu, the first step is to extract its icon.
Icons are resources (.rsrc) in the .exe binary that are accessible via Win32 APIs LoadLibrary() and EnumResourceNames().
Once extracted, you obtain a Windows .ico file which might not be recognized by Linux Desktop environments.
You need to convert the .ico to a PNG. For this I am using Win32 GDI+ API.
Finally, place the .png in /home/user/.local/share/icons for use in the coming sections.
The full code for the .exe icon extraction can be found in icostrip.inc, in the ICOstrip() function.
Showing an .exe icon in Linux file explorer
To display an icon/image on a file in the Linux file explorer e.g. in Nautilus, the following gio <?> command can be used:
$> gio set -t 'string' '/path/to/file' 'metadata::custom-icon' 'file:///path/to/icon'
The /path/to/icon is of course obtained from the previous step linking to /home/user/.local/share/icons/myprogram.png.
The /path/to/file needs to be a Unix path but since we are running inside a Win32 program via Wine we need to convert it,
see Translating between Unix and Windows paths.
Here is Nautilus in normal conditions, always showing the same (default) icon for all .exe files:
And here is Nautilus after using the gio method from inside my programs (at startup when they run):
(see section Calling native Unix commands from a Win32 .exe on how we are able to call gio from inside the Win32 .exe.)
The full code for revealing an .exe icon can be found in exe2unix.inc, in the IcoStrip2Unix() and FileIco2Unix() functions.
Showing an .exe icon in the Linux Dock
The Linux Dock conveniently lists the pinned Apps and the currently running Apps. On Ubuntu 24.04 it looks like this: As you can see, it only shows icons and not program names, so all .exe look the same! In order to display the .exe icons in the dock, the first step is of course to extract the .exe icon and convert it to PNG (Extracting .exe icons). The next trick is to write a.desktop file in /home/user/.local/share/applications.
The .desktop entry specification can be found at https://specifications.freedesktop.org/desktop-entry-spec/latest/
The key element to add in the .desktop is the Startup WM Class (see Looking Glass debugger) that identifies a running program.
For Win32 .exe via Wine it's particularly easy: the WM Class is the full name of the exe in small caps e.g. vlc playlist manager.exe.
As an example here is the .desktop for my program img-rnd:
[Desktop Entry]
Name=img-rnd
StartupWMClass=img-rnd.exe
NoDisplay=true
Type=Application
Terminal=false
StartupNotify=true
Icon=img-rnd
The NoDisplay=true typically allows to NOT register the program in the Linux App Menu.
Concerning the icon, the main differences with the gio method (used to display an .exe icon in the file explorer) is that:
- only PNG images are supported by .desktop entries (
giocould use .ico files, although I made it use .png for homogeneity) - the icon image needs to be listed without its extension:
Icon=img-rnd
Here is the Linux Dock after applying this technique to my programs: much better!
The full code for .exe icons in the Dock can be found in exe2unix.inc, in the Exe2Unix() function.
Registering an .exe in the Linux App Menu
The principle is the exact same as the previous section, with two modifications:
- the
NoDisplay=truemust be removed from the .desktop entry, of course - and we need to add an
Execvalue to launch the .exe through Wine
That last part was tricky to achieve, but here is how it looks for my program linkmaker:
[Desktop Entry]
Name=linkmaker
StartupWMClass=linkmaker.exe
Exec=env WINEPREFIX="/home/user/.wine" wine C:\\\\Apps\\\\linkmaker.exe
Type=Application
Terminal=false
StartupNotify=true
Icon=linkmaker
Exec value has its backslashes protected twice. If the .exe name contained spaces, they should be protected too by a double backslash, e.g.: C:\\\\Apps\\\\VLC\\ Playlist\\ Manager.exe.
And here is the Linux App Menu in Ubuntu 24.04, once we registered our Win32 .exe programs that way:
The full code for .exe registration can be found in exe2unix.inc, in the Me2Unix() function.
Translating between Unix and Windows paths
Since were are running our .exe programs through Wine the drives and paths are different from native Linux.
If you are a Wine user, you may know that the Z:\ drive in Wine is mapped to the Unix root /, and the C:\ drive to /home/user/.wine/drive_c (the Wine installation path, where all Windows files are stored). So for example if you want to access the Shell Script interpreter /usr/bin/sh from inside our Win32 .exe,
e.g. for testing its existence, this will be done through Z:\usr\bin\sh.
Hence the conversion from Unix to Wine path is trivial:
| Unix | Wine |
|---|---|
| /path/to/file | Z:\path\to\file |
| ~/path/to/file | Z:\home\user\path\to\file |
The conversion from Wine to Unix is a bit more complicated:
| Wine | Unix |
|---|---|
| Z:\path\to\file | /path/to/file |
| C:\path\to\file | /home/user/.wine/drive_c/path/to/file |
| F:\path\to\file | Replace "F:" with the Wine mapping for this drive |
The last part, for drives different from "C:" and "Z:", needs to rely on our method for mapping Wine drives to Unix paths.
We call GetWineDrivesMapping() at the start of our application, then we find the correspondance between the drive and Unix path and apply the change.
Finally, if the drive is not registered as a Wine drive somehow, we fall back on a manual correspondance "X:" (Wine) >> "/home/user/.wine/dosdevices/x:" (Unix).
The full code for paths translation can be found in unix.inc, in the UnixPath() and WinePath() functions.
Mapping Wine drives to Unix paths
As seen in the previous section, we need to get Wine mappings from drives to Unix paths, in order to access files from both worlds.
Some mappings are done natively by Wine, others can be added manually by the user by typing the command winecfg in the Linux terminal and going to the "Drives" tab:
Adding additional drive mappings is a must if you want your Win32 .exe program to access files from an external Hard Disk Drive.
This is typically what I did for my portable HDD containing all my favorite TV Shows: mapping it to F: so that my program VLC Playlist Manager can run undisturbed.
The way to get all drive mappings is through a Unix command, for which we need the output: see getting Unix command output from a Win32 .exe. The command in question, to be typed from the user home, is:
$> ll .wine/dosdevices(or ls -al if you don't have the ll alias)
The result of the command looks like this:
From there, a simple parser allows to map relevant drives to their Unix path. I used a global array dosdevices() to store the mappings, in the form "d:/media/user/myhdd".
The full code for drives mapping can be found in unix.inc, in the GetWineDrivesMapping() function.
Calling native Unix commands from a Win32 .exe
Where our Win32 .exe can become very powerful is when it is able to launch native Unix commands and break the walls imposed by Wine.
This is a feature we use a lot: whether to call gio in order to assign an icon to a file (Showing an .exe icon in Linux file explorer),
or using ls to list Wine drive mappings in ".wine/dosdevices",
or calling the native Linux VLC Media Player with its Unix path, targetting a (Win32 .exe) generated playlist...
The way we do it is via a dynamically generated Shell script. We use the standard Unix command language interpreter Shell (sh): https://www.man7.org/linux/man-pages/man1/sh.1p.html
The Shell allows us to do practically anything in the Linux world, even taking advantage of the native ENVIRONMENT variables (unaccessible from Wine).
Native ENV vars allow us to type e.g. sh ~/myscript.sh in a script, instead of the fully resolved /usr/bin/sh /home/user/myscript.sh.
Even more powerful: we can identify if any Unix binary is present, as well as its location, via the native command whereis unix_bin and act in consequence.
Our dynamic script is written in the user home, as ~/dues.sh (dues = dynamic unix execution script).
After writing it, we make it executable in order to run it in Linux, this is done via the following Wine command:
wine start /exec /usr/bin/chmod +x /home/user/dues.sh(as indicated previously, Wine does not have access to ENV so we need to write fully resolved paths.)
We overwrite the ~/dues.sh script every time we run a different command, and we call wine start /exec to launch it.
The full code for native Unix calls can be found in unix.inc, in the CreateExecSh() and UnixShell() functions.
Getting Unix command output from a Win32 .exe
The biggest part being done: calling native Unix commands from a Win32 .exe, we use this technique to run a command and
redirect its output to a file with the > operator:
In our case, we use the temporary file ~/dues.log in the user home.
After we launch the ~/dues.sh script, we poll on ~/dues.log every 250 ms, with a timeout of 3s:
if the timeout is reached, we consider the command failed and there was no outptut ;
on the contrary if the file ~/dues.log contains a result, we extract it and return it as the output of the Unix command.
Finally we clean-up the ~/dues.log file.
The full code for Unix output can be found in unix.inc, in the GetUnixRet() function.
Creating Windows shortcuts on Linux
If you double-click on an .exe file in Ubuntu 24.04 / Nautilus, nothing happens. To launch any .exe you need to right-click it then pick "Open with..." and double-click on "Wine"... That is tedious to say the least. The big advantage of Windows shortcuts is that when Wine is installed, they can be launched via double-click.
You can create a symbolic link (symlink) by selecting a file in the file explorer and do Ctrl+Shift+M or using the ln command in a terminal,
but not only the symlink on an .exe won't solve the double-click issue, but also the program will only be able to execute from the symlink
working directory, which can bring lots of issues.
As a matter of fact, Windows shortcuts have an extra field "Start in" which allow to use .dll's, .ini's, and any other resource located around the program, what symlinks cannot do...
I wrote the linkmaker.exe program for this exact use-case: create Windows shortcuts (.lnk) that launch under Linux via double-click, using all the techniques documented above including showing the .lnk with the icon of the .exe it targets. This is something I use all the time after writing a new utility: easily make a Windows shortcut of it on my Ubuntu desktop for quick access.
Looking Glass debugger
The Looking Glass is a basic debugger integrated in Gnome, see https://wiki.gnome.org/Projects/GnomeShell/LookingGlass .
You use it by pressing Alt+F2 then typing lg (for looking glass) then Return, and looking in the Windows tab.
One important field shown is the wmclass field, used as an identifier by the .desktop files.
This is useful when running Linux Apps, but somehow less for Windows executables, as their WM Class is trivial (their name in small caps...)
Coming from Windows, I was used to get the same kind of information from a program called WinSpy from catch22.
Hide HDD folders like "System Volume Information" or "RECYCLER"
Coming from Windows, these folders are automatically hidden on external hard drives, but not on Linux.
To do the same on Linux is quite easy actually:
just create a text file called .hidden on your external hard drive, and add folders you wish to hide, 1 per line, e.g.:
found.000
$RECYCLE.BIN
RECYCLER
System Volume Information
After that press Ctrl+H to see/hide hidden files+folders in your Linux file explorer, and hit F5 to refresh the view.










