English | 简体中文
How to build ImGui4NET from source. The two stages are independent: generating bindings (running ClangSharp) and compiling native libraries (running the C++ compiler).
# 1. Fetch submodules (imgui + imgui_club)
git submodule update --init --recursive
# 2. Restore the pinned dotnet tools (ClangSharpPInvokeGenerator)
dotnet tool restore
# 3. Download the NuGet-hosted native dependencies (Allegro / freeglut)
./tools/fetch-deps.sh
# 4. Generate bindings and compile desktop native libraries
./tools/generate.sh --native
# 5. Build the managed assemblies
dotnet build ImGui4NET.slnxOnly want the NuGet packages:
dotnet pack ImGui4NET.slnx -c Release -o ./artifacts
dotnet tool restoremust run before the firstgenerate.sh. It readsdotnet-tools.jsonand installsClangSharpPInvokeGeneratorat the pinned version, so generated code stays reproducible across machines. Skipping it produceserror: dotnet tool 'ClangSharpPInvokeGenerator' not found.
| Tool | Purpose | Version |
|---|---|---|
| .NET SDK | Build managed code, run ClangSharp | 10.0+ |
| bash | Run tools/*.sh |
4.0+ |
| python3 | Post-process generated output | 3.9+ |
| C++17 compiler | Compile native libraries | g++ / clang++ / MSVC's clang |
| git | Submodules | any |
tools/generate.sh probes g++ → clang++ → c++ on its own. Override with
the CXX environment variable.
| Platform | Compiler | Notes |
|---|---|---|
| Windows | MSYS2 MinGW64 g++ | Typically D:/Softwares/msys2/mingw64 |
| Linux | system g++ | Needs build-essential libgl1-mesa-dev libglfw3-dev libsdl2-dev libvulkan-dev freeglut3-dev liballegro5-dev |
| Android | NDK clang | See the dedicated section below |
| macOS | clang + ld64.lld |
Cross-builds from any host; see macOS |
ClangSharp needs a clang resource directory. The script searches
MINGW_ROOT/lib/clang/*, /usr/lib/clang, /usr/lib/llvm-*/lib/clang. You can
also set it explicitly:
export CLANG_RESOURCE=/path/to/clang/22./tools/generate.sh # generate bindings only
./tools/generate.sh --native # generate + build desktop natives
./tools/generate.sh --android # generate + build Android arm64 natives
./tools/generate.sh --macos # generate + build macOS natives
./tools/generate.sh --all # generate + desktop + Android + macOS
./tools/generate.sh --native --no-generate # build natives only, keep bindings
./tools/generate.sh --helpThe bindings are generated with a Windows target triple
(x86_64-w64-windows-gnu), so every DllImport name is LLP64-mangled:
_ZN5ImGui8MemAllocEy ← y = unsigned long long (Windows size_t)
Running the full generate.sh on Linux regenerates everything with
x86_64-unknown-linux-gnu, which mangles size_t as m. That silently breaks
the Windows package, whose EntryPoint strings no longer resolve.
So when cross-building:
# Generate once on Windows and commit the result
./tools/generate.sh
# On Linux / Android only rebuild the native libraries
./tools/generate.sh --native --no-generate
./tools/generate.sh --android --no-generateThe LP64 spelling differences are patched at link time by
tools/lp64-aliases.py using --defsym; see Symbol aliases.
| Variable | Default | Purpose |
|---|---|---|
CXX |
auto-detected | C++ compiler |
PYTHON |
python3 |
Interpreter for the post-processing script |
MINGW_ROOT |
— | Helps locate the clang resource directory |
CLANG_RESOURCE |
auto-detected | clang resource directory |
ANDROID_NDK |
auto-detected | NDK root |
ANDROID_API |
24 |
Android API level |
TOOLCHAIN_PREFIX |
derived from CXX |
Locates nested headers such as SDL2 |
MACOS_SDK_DIR |
third_party/macos-sdk/<version>/MacOSX<version>.sdk |
macOS SDK to build against |
MACOS_SDK_VERSION |
26.0 |
SDK version downloaded when MACOS_SDK_DIR is unset |
MACOS_MIN_VERSION |
11.0 |
-mmacosx-version-min / -platform_version |
MACOS_ARCHES |
x64 arm64 |
Architectures to build |
MACOS_CXX |
auto-detected clang | Compiler used for macOS objects |
MACOS_LD |
auto-detected ld64.lld |
Mach-O linker |
graph TD
A["imgui/imgui.h<br/>imgui/imgui_internal.h"] --> B["Synthetic TU<br/>imgui_tu.h"]
B --> C["ClangSharpPInvokeGenerator"]
C --> D["Raw .cs"]
D --> E["postprocess.py"]
E --> F["src/ImGui4NET/Generated/*.cs<br/>one file per type"]
K["backends/imgui_impl_*.h"] --> C
C --> E
E --> L["src/ImGui4NET.Backend.*/Generated/*.cs"]
M["src/ImGui4NET.Club/*.cs"] --> N["Pure C# port<br/>no generation step"]
Generating from imgui.cpp drops every function that has a body
(--generate funcs-with-body=false). Generating from imgui.h alone misses the
full definitions of internal structs such as ImGuiContext and ImGuiWindow.
A synthetic header that includes both is the only way to get everything in one pass:
// imgui_tu.h
#include "imgui.h"
#include "imgui_internal.h"| Flag | Meaning |
|---|---|
-f |
Parse entry point (translation unit) |
-t |
Header allowed to emit output |
This is why imgui.h is deliberately not listed under -t for a backend:
# imgui.h is parsed as a dependency but not listed under -t
-t "backends/imgui_impl_glfw.h"Adding imgui.h to -t duplicates every Dear ImGui type into the consumer
package and collides with the core assembly (measured in the club package: 266
lines grew to 5112). ImGui4NET.Club needs no generation step at all any more,
since it is a pure C# port.
ClangSharp emits "C++-shaped C#", which needs fixing before it compiles:
| Rewrite | Input | Output |
|---|---|---|
| Template expansion | ImVector<T> / ImBitArray / ImSpan / ImPool / ImChunkStream / ImStableVector / ImSmallStack |
Equivalent fixed-layout fields |
| Trailing underscore | ImDrawFlags_ |
ImDrawFlags |
| String parameters | sbyte* label |
string label (LPUTF8Str) |
| String defaults | = "%.3f"u8 |
= null |
| Bool defaults | byte x = false |
byte x = 0 |
| Negative masks | ~0x7FFFFFF0 |
-2147483633 |
| File pointer | _iobuf* |
void* |
| Struct safety | partial struct |
unsafe partial struct |
| Constructors | Foo(Foo* pThis) |
Foo_Ctor(...) |
| Type prefixes | ImGui_ImplDX11_RenderState |
RenderState |
| Method prefixes | ImGui_ImplOpenGL3_Init |
Init |
| Splitting | Single file | One file per type |
The trailing-underscore rewrite must protect
EntryPoint = "..."strings first. Mangled symbols can legitimately end with_(_Z10ImFileOpenPKcS0_), and trimming it yields a runtimeEntryPointNotFoundException.
./tools/generate.sh --nativeOutput locations:
src/ImGui4NET/runtimes/<rid>/native/imgui.dll | libimgui.so
src/ImGui4NET.Backend.*/runtimes/<rid>/native/imgui_backend_*.dll | lib*.so
Link libraries per platform:
| Backend | Windows | Linux |
|---|---|---|
| OpenGL2 / OpenGL3 | -lopengl32 |
-lGL |
| Glfw | -lglfw3 -lopengl32 -lgdi32 |
-lglfw -lGL |
| Glut | -lopengl32 -lfreeglut |
-lGL -lglut |
| Sdl2 | -lSDL2 |
-lSDL2 |
| Sdl3 / SdlGpu3 | -lSDL3 |
-lSDL3 |
| Vulkan | -lvulkan-1 |
-lvulkan |
| DirectX9/11/12 | -ld3d9 / -ld3d11 -ldxgi / -ld3d12 -ldxgi + -ld3dcompiler |
— |
| Win32 | -ldwmapi -lgdi32 -limm32 -lole32 |
— |
export ANDROID_NDK=/d/AndroidSDK/android-ndk-r27c
./tools/generate.sh --androidThe script locates aarch64-linux-android${ANDROID_API}-clang++ and prefers the
extension-less binary: MSYS2 ships both, and only the extension-less one is
directly executable from bash.
The API level must be 24 or higher, because libvulkan.so first appears at
24. Lower values fail to link the Vulkan backend.
Backends available on Android:
| Backend | Available | Reason |
|---|---|---|
| OpenGL3 | ✅ | Links -lGLESv2 -lEGL |
| Vulkan | ✅ | -lvulkan |
| Android | ✅ | -landroid -llog |
| OpenGL2 | ❌ | Fixed-function pipeline needs GL/gl.h; NDK only ships GLES |
| Glfw / Sdl2 / Sdl3 / Glut / Allegro5 | ❌ | No NDK headers |
./tools/generate.sh --macos --no-generateBuilds libimgui.dylib for osx-x64 and osx-arm64, plus the OpenGL2,
OpenGL3 and Glut backends. It runs on Windows or Linux — no Mac required.
The SDK is the one unusual requirement. Linking a Mach-O needs Apple's
system headers, the libc++ headers and the .tbd stubs that stand in for the
real shared libraries. None of those come with a normal host toolchain, so
tools/fetch-macos-sdk.sh downloads the republished SDK tarball on first use:
./tools/fetch-macos-sdk.sh # into third_party/macos-sdk/26.0/
./tools/fetch-macos-sdk.sh /opt/sdk # or somewhere elsegenerate.sh --macos calls it automatically when MACOS_SDK_DIR does not
exist. The SDK is ~1.4 GB unpacked and git-ignored.
Backends available on macOS:
| Backend | Available | Reason |
|---|---|---|
| OpenGL2 / OpenGL3 | ✅ | -framework OpenGL |
| Glut | ✅ | -framework OpenGL -framework GLUT -framework Cocoa |
| Glfw / Sdl2 / Sdl3 / Vulkan | ❌ | Headers are not part of the SDK; vendoring them is not done yet |
| Metal | ✅ | -framework Metal -framework MetalKit -framework Cocoa -framework QuartzCore |
| Metal4 | ✅ arm64 only | -framework Metal -framework QuartzCore -framework Cocoa; requires macOS 26 and Apple Silicon |
| OSX | ✅ | -framework Cocoa -framework GameController -framework Metal -framework QuartzCore |
Three macOS specifics the script handles:
@loader_pathinstall names. A backend records its dependency on the core library as@loader_path/libimgui.dylib, so the pair resolves from the sameruntimes/<rid>/nativefolder no matter where the package is unpacked.- LP64 aliases via assembly.
ld64.lld -aliascannot bind to a symbol defined by an object file, solp64-aliases.py --format asmemits a.sfile that the assembler turns into real aliases. x64 needs 46, arm64 needs 29 —va_listalready differs between the two (P13__va_list_tagvsPc). - Deployment target.
-mmacosx-version-minbeside-platform_version;MACOSX_DEPLOYMENT_TARGETdoes not reach a directld64.lldinvocation.
These are two independent things, and only the first decides whether a library loads on an older macOS:
| Field | Where it comes from | Meaning at run time |
|---|---|---|
minos |
-mmacosx-version-min / -platform_version |
Minimum OS the dylib loads on |
sdk |
The SDK tarball used to compile | Recorded only; never consulted |
So an SDK 26 build with MACOS_MIN_VERSION=11.0 produces minos=11.0,
sdk=26.0, and loads on macOS 11. Verified on the shipped libraries:
# What the linker recorded
python - <<'PY'
import struct
from pathlib import Path
d = Path("src/ImGui4NET/runtimes/osx-arm64/native/libimgui.dylib").read_bytes()
ncmds = struct.unpack_from("<I", d, 16)[0]
off = 32
for _ in range(ncmds):
cmd, size = struct.unpack_from("<II", d, off)
if (cmd & 0x7fffffff) == 0x32: # LC_BUILD_VERSION
_, minos, sdk, _ = struct.unpack_from("<IIII", d, off + 8)
f = lambda v: f"{v >> 16}.{(v >> 8) & 0xff}"
print(f"minos={f(minos)} sdk={f(sdk)}")
off += size
PYTwo further checks back this up, both reproducible:
- Imports are identical. A core dylib built from SDK 14.5 and from SDK 26.0
has the same 59 undefined symbols (
_malloc,_snprintf,_sinf, …). No new API from a newer macOS leaks in, because Dear ImGui only uses libc and libc++. - No weak imports. A binary that opportunistically used newer APIs would
carry
N_WEAK_DEFundefined symbols, which dyld tolerates. These libraries have zero.
Where it does bite: an SDK 26 header that marks a symbol macOS 26-only. Those
become hard dependencies the moment the code calls them, and the compiler only
warns (-Wunguarded-availability-new). This is exactly why Metal4 is the one
backend with a per-backend deployment target:
| Backend | -mmacosx-version-min |
minos |
Architectures |
|---|---|---|---|
| OpenGL2 / OpenGL3 / Glut / Metal / OSX | MACOS_MIN_VERSION (11.0) |
11.0 | x64 + arm64 |
| Metal4 | 26.0 |
26.0 | arm64 only |
imgui_impl_metal4.mm binds MTL4RenderPassDescriptor and MTL4CommandQueue,
both API_AVAILABLE(macos(26.0)), with no runtime guard in the source. Compiling
it with an 11.0 floor merely silences the availability warnings — the resulting
dylib would still be accepted by dyld on macOS 11 and then fail on the first
symbol lookup. Recording minos=26.0 turns that into an honest load-time
rejection. It is also arm64-only, because Metal 4 requires Apple Silicon.
Both constraints live in the OBJC_BACKENDS table in generate.sh:
# name|header|frameworks|opaque types|deployment target|architectures
"Metal4|imgui_impl_metal4.h|-framework Metal ...|MTL4RenderPassDescriptor ...|26.0|arm64"Verified on the shipped library:
LC_BUILD_VERSION platform=1 minos=26.0.0 sdk=26.0.0
ld64.lld ships in the MSYS2 clang package:
pacman -S mingw-w64-x86_64-lldMetal, Metal4 and OSX are the only backends written in Objective-C++, and
that changes three things:
-
Generation. Their headers declare the API only inside
#ifdef __OBJC__, so the generator is invoked with-x objective-c++(tools/gen-objc-backend.sh, called fromgenerate.sh). -
Handle types. Objective-C classes and protocols (
id<MTLDevice>,MTL4CommandBuffer,NSView) have no C# equivalent, and ClangSharp emits no declaration for a type it only sees forward-declared — it falls back tovoid*.postprocess.pytherefore generates areadonly structper handle into the backend'sGenerated/folder:public readonly partial struct NSView { private readonly nint _h; public NSView(nint handle) => _h = handle; public nint Handle => _h; public bool IsNull => _h == 0; public static NSView Null => default; }
The struct is a single machine word, so it is blittable and the extern takes it by value — the ABI passes the pointer the header declares, with no wrapper method and no marshalling:
[DllImport("imgui_backend_osx", EntryPoint = "_Z22ImGui_ImplOSX_NewFrameP6NSView")] public static extern void NewFrame([NativeTypeName("NSView * _Nullable")] NSView view);
Nullability is carried in the
NativeTypeNameattribute for documentation; because the handle is a value type, a null one isNSView.Null(ordefault) rather than a nullable reference. -
Symbol names. Every C symbol in a Mach-O symbol table carries a leading underscore, and ClangSharp copies that spelling into
EntryPoint.dlsym()wants the C name (dlsym(3)says the name "must NOT be prepended with an underscore"), sopostprocess.py --macho-symbolsstrips it again. The_Z…body of the mangled name is untouched.
Method names drop the C prefix like every other backend: ImGui_ImplMetal4_Init
becomes ImGui_ImplMetal4.Init. A bool return stays a byte, matching the
other packages in this repository rather than wrapping it into a managed bool.
ClangSharp reports Unsupported declaration: 'ObjCInterface' / 'ObjCProtocol'
for those forward declarations and exits non-zero. That is expected: the script
treats an empty output file as the failure condition, not the exit code.
CI does not build any of these libraries. Like the other RIDs, the .dylib files
are committed under src/**/runtimes/osx-*/native/.
ImGui4NET.Club ships no native library. The three club extensions are
ported to plain C# instead of bound through P/Invoke, because every one of their
methods is defined inside its class body:
struct MemoryEditor {
void DrawWindow(const char* title, void* mem_data, ...) { ... } // inline in class
};That causes two problems:
- ClangSharp emits no extern —
funcs-with-body=falseskips functions with bodies - The compiler emits no symbol — an inline function only materialises in a TU that calls it
Forcing those bodies out of a native library (a -fkeep-inline-functions
compilation unit plus -O0, and -femit-all-decls on Clang) works, but it is a
lot of machinery to bind code that only ever calls the public Dear ImGui API.
The port was therefore simpler than the binding:
| Extension | Port |
|---|---|
imgui_memory_editor.h |
MemoryEditor.cs — MemoryEditor |
imgui_multicontext_compositor.h |
ImGuiMultiContextCompositor.cs — MultiContextCompositor |
imgui_threaded_rendering.h |
ImDrawDataSnapshot.cs and ImTextureQueue.cs |
Two points about the ports are not obvious:
ImVectormutations must useImGui.MemAlloc/MemFree. A draw list whoseCmdBufferwas allocated by the CLR cannot be handed back to Dear ImGui, soImVectorUtil.csreproducesreserve/resize/push_back/swap/clearagainst Dear ImGui's allocator.ImTextureQueue's per-texture records live in native memory. Their address is stored inImTextureData.QueueUserDataand dereferenced by the render thread, so a managed object would move.
generate.sh prints imgui_club: pure C# port, nothing to generate and does
nothing for this package: there is no SDK step, no native build, and no RID
folder.
The bindings are LLP64-generated. LP64 platforms differ in three places:
| Type | Windows (LLP64) | x86_64 Linux | aarch64 Android | macOS x64 | macOS arm64 |
|---|---|---|---|---|---|
size_t |
y |
m |
m |
m |
m |
FILE |
_iobuf |
_IO_FILE |
__sFILE |
__sFILE |
__sFILE |
va_list |
Pc / S_ |
P13__va_list_tag |
St9__va_list |
P13__va_list_tag |
Pc |
tools/lp64-aliases.py diffs the entry points the bindings expect against the
symbols the built library actually exports, then prints linker arguments:
-Wl,--defsym=_ZN5ImGui8MemAllocEy=_ZN5ImGui8MemAllocEmMach-O has no linker switch that can alias a symbol defined by an object file,
so --format asm emits an assembly file instead:
.globl __ZN5ImGui8MemAllocEy
.set __ZN5ImGui8MemAllocEy, __ZN5ImGui8MemAllocEmOne binding set therefore serves all platforms.
The script applies to the core library and every native backend library. It iterates to a fixed point, since each relink can expose more mappable symbols:
for attempt in 1 2 3 4; do
link → recompute aliases → exit when the count stops growing
donebuild/ # not committed, excluded via .gitignore
implib/<rid>/libimgui.dll.a # MinGW link-time only, never packed
logs/<name>.log # compiler logs; check these on failure
macos-obj/<arch>/*.o # macOS objects and the generated alias .s
src/ImGui4NET/
Generated/ # 212 files, one type per file
runtimes/<rid>/native/ # native libraries
ImVector.cs # hand-written: C++ templates cannot be generated
NativeTypeNameAttribute.cs # hand-written: must be public to be shared
src/ImGui4NET.Club/
MemoryEditor.cs # hand-written: pure C# port
ImGuiMultiContextCompositor.cs
ImDrawDataSnapshot.cs
ImTextureQueue.cs
ImVectorUtil.cs # hand-written: ImVector ops on ImGui's allocator
src/ImGui4NET.Backend.*/
Generated/ # a few files per backend
runtimes/<rid>/native/ # that backend's native library
ClangSharp needs clang's built-in headers. Point it at them:
export CLANG_RESOURCE=D:/Softwares/msys2/mingw64/lib/clang/22The scripts have CRLF line endings. The repo ships a .gitattributes forcing
*.sh eol=lf; if it is still broken:
sed -i 's/\r$//' tools/*.shThe aliases did not apply. Check:
nm -D --defined-only src/ImGui4NET/runtimes/linux-x64/native/libimgui.so | grep _ZN5ImGui8MemAllocEyBoth ...Ey and ...Em should be present.
This placeholder no longer exists: imgui_club is a pure C# port and builds no
native library on any platform. If an old build output still references
libimgui_club.so or imgui_club.dll, delete the stale runtimes/ folder
under src/ImGui4NET.Club/.
Run dotnet tool restore from the repository root.
Linux and Android require -Wl,--export-dynamic (already supplied by the
script); otherwise the dynamic symbol table is incomplete.
# Managed layer (net10.0; Windows backends also build net10.0-windows)
dotnet build ImGui4NET.slnx
# Runtime smoke test
dotnet run --project samples/ImGui4NET.Sample.OpenGL3The sample also drives the imgui_club Memory Editor through its pure C# port,
so it exercises ImGui4NET.Club (MemoryEditor, no native library) at the
same time.
Quick check of exported symbol counts:
# Windows
objdump -p src/ImGui4NET/runtimes/win-x64/native/imgui.dll | grep -c '_Z'
# Linux / Android
nm -D --defined-only src/ImGui4NET/runtimes/linux-x64/native/libimgui.so | grep -c '_Z'Expected: roughly 1200+ for the core library.
Publishing runs in CI (.github/workflows/publish.yml) on a v* tag, using
NuGet Trusted Publishing:
the workflow exchanges a GitHub OIDC token for a temporary API key, so no
long-lived secret is stored in the repository.
One-time setup on nuget.org:
- Account → Trusted Publishing → add a policy.
- Repository Owner
StArraySharp, RepositoryImGui4NET, Workflow Filepublish.yml, Environmentrelease. - Add a repository secret
NUGET_USERholding your nuget.org profile name (not the email address). - Request prefix reservation for
ImGui4NETso nobody else can publish underImGui4NET.*.
Releasing a version:
# 1. Bump LibraryRevision in src/Directory.Build.props if only bindings changed,
# or ImGuiVersion when moving to a new Dear ImGui tag.
# 2. Update docs/CHANGELOG.md.
# 3. Tag and push; the workflow packs and publishes.
git commit -am "release: 1.93.0.2"
git tag v1.93.0.2
git push origin main --tagsThe workflow refuses to publish when a package version differs from the tag.
Local dry run:
dotnet pack ImGui4NET.slnx -c Release -o ./artifacts
# inspect the contents before pushing
unzip -l artifacts/ImGui4NET.1.93.0.1.nupkg