Generate C Bindings from a Mono Assembly with cilc

cilc turns a compiled Mono CIL assembly into C source files that expose its classes through a plain C interface. This guide prepares the two paths it expects, runs it, and inspects the result before you trust it, ready for compiling into a shared object. Allow 15 to 30 minutes for a first run, more if you still need to build or package the assembly.

This guide describes the cilc(1) manual installed with Ubuntu's mono-devel package version 6.8.0.105+dfsg-3.6ubuntu2. The local manual documents the positional interface but says nothing about named options, the generated file list, or a sample assembly. So the examples keep input and output names explicit and lean on inspection commands to see what this particular build actually emits.

1. Check the package and the executable

Start without changing anything. A package can be installed even when its command is missing from the current PATH, and the manpage being present proves nothing about the executable:

$ dpkg-query -W -f='${Package} ${Version}\n' mono-devel
mono-devel 6.8.0.105+dfsg-3.6ubuntu2
$ command -v cilc
/usr/bin/cilc

Your version and path may differ. If command -v prints nothing, stop here and investigate through your normal package-management process. Do not substitute mono, ilasm, or monodis: they do different jobs entirely. On this machine the manpage is installed but the cilc executable is not, so a real generation run cannot be verified locally.

Checkpoint: continue only once command -v cilc returns the executable you intend to run, and you've recorded the package version for your build notes.

2. Confirm the two positional arguments

The synopsis is cilc [options] assembly target. The first positional argument is the input CIL assembly; the second is the target for the generated C sources. The description says the result is a directory, so point it at something new and empty rather than a source tree you actually care about.

$ cilc /path/to/example.dll /path/to/cilc-output

The manual defines no option names or defaults here. Do not borrow guessed switches from another binding generator. If your build ships a separate help screen or vendor documentation, read that before putting anything into a script.

Use an assembly you're actually allowed to inspect and bind. An assembly is not necessarily self-contained: it can reference other managed libraries, and the bindings it generates can expose types whose runtime dependencies aren't present on a target machine.

3. Create a fresh output directory safely

Make the destination explicitly and refuse to reuse a non-empty directory, so you never mix stale generated files with a new run. These commands only create a directory below the current working directory and need no elevated privileges:

$ mkdir -p ./cilc-output
$ if find ./cilc-output -mindepth 1 -print -quit | grep -q .; then
>     printf '%s\n' 'cilc-output is not empty; choose another directory' >&2
>     exit 1
> fi
$ test -d ./cilc-output && echo 'empty output directory is ready'
empty output directory is ready

You don't need sudo when the assembly and destination belong to your user. If either path sits under a protected system directory, fix the working location or its permissions instead of running the generator as root.

Safety warning: do not reach for a recursive removal command just to make the example work. If you need to discard generated output later, inspect the exact path with pwd and find first, then remove only files you can identify as generated. The output can be recreated; deleting the wrong directory cannot always be undone.

4. Run the generator and inspect its result

Replace both placeholders with real paths, and quote them so spaces in a project directory don't shift the argument boundaries:

$ ASSEMBLY='/path/to/example.dll'
$ TARGET="$PWD/cilc-output"
$ cilc "$ASSEMBLY" "$TARGET"
$ printf 'cilc exit status: %s\n' "$?"
cilc exit status: 0
$ find "$TARGET" -maxdepth 2 -type f -print | sort

A zero status means the command completed. The precise filenames aren't specified by the local manpage, so treat the find listing as the authoritative answer for this installed build: you should see C source files and may see headers or supporting files. If the command writes diagnostics, keep them with the build log rather than assuming every warning is harmless.

If the target directory is still empty after a zero status, stop and investigate before compiling anything. Check that you passed the assembly and target in the right order, then consult the build's documentation or source. Do not silently point a compiler at an unrelated directory.

5. Check the generated code's runtime boundary

cilc generates sources that use the Mono embedding API. That means the manual requires a complete Mono development environment on whichever system compiles these sources, and the eventual target system needs a complete Mono runtime environment too. These are separate requirements: a C compiler alone does not satisfy the embedding API dependency.

$ find "$TARGET" -maxdepth 2 -type f \( -name '*.c' -o -name '*.h' \) -print | sort
$ pkg-config --list-all 2>/dev/null | grep -i '^mono' || true
$ mono --version

The first command confirms the generator produced the source types you plan to compile. The other two are environment probes, not a documented cilc validation procedure, and their output depends on how Mono was packaged. If the development metadata is missing, resolve that on the build host before attempting a shared library build.

Review the generated files before compiling them. They're build inputs, not a substitute for an API design review. Confirm the assembly's public surface is appropriate for a C interface and that the generated names don't collide with symbols already in your project.

6. Keep the build and deployment checks separate

The generator only creates C sources; the local manual is explicit that it does not compile them or install a shared object. Use your project's normal compiler and linker configuration once you've inspected the output, and record the Mono headers, libraries, compiler flags, architecture and assembly dependencies that build used.

Before deploying a resulting shared object, test it in a disposable environment with the same Mono runtime family as the target. A library that compiles cleanly can still fail when the target lacks a managed dependency or an embedding-runtime component. Don't copy generated libraries into a system directory as an experiment: use a staging directory and an explicit rollback plan.

To undo this guide's filesystem change, remove only the dedicated output directory after checking its contents:

$ pwd
$ find ./cilc-output -maxdepth 2 -type f -print
$ rmdir ./cilc-output 2>/dev/null || printf '%s\n' 'directory contains files; review before removing it'

rmdir removes the directory only when it's empty. If it contains generated files, leave them for review or remove each confirmed generated file individually. The original assembly is never modified by anything above.

Done means