- Author: Douglas P. Fields, Jr.
- Copyright 2026 Douglas P. Fields, Jr.
- License: Apache 2.0 - see LICENSE
dotcl-packagegen is a standalone CLI tool (packaged as a dotnet tool) that generates
DotCL Common Lisp packages/bindings for arbitrary .NET
assemblies. It is a hybrid C#/Common Lisp codebase: C# does .NET reflection and hosts the
DotCL Lisp runtime; Lisp does the actual code generation via string templating.
A single invocation generates everything for one or more assemblies in one pass:
-
Metadata reflection (pure C#, no DotCL needed): for each
--assembly,AssemblyToLispy.csreflects over the .NET assembly (plus its sidecar.xmldoc file, if present) and emits a single Lisp-reader-compatible s-expression list — one plist per public type — to a<AssemblyName>.lispy.metadatafile in--out-dir. Seedoc/assembly-to-lispy.mdfor the complete, canonical schema of this format (keys, flags, parameter plists, documentation plists, default-value literal formatting, etc.) -
Package generation (boots DotCL): once every assembly's metadata has been reflected, the metadata plus the requested classes are handed to
assembly-package-generator.lisp(run-assembly-package-generator-batch→generate-assembly-packages-batch→generate-class-file), which emits a.lispfile per requested class defining a package with idiomatic Lisp wrapper functions for that C# type's constructors, methods, properties, and fields. The same invocation also writespackages.lisp(every generated package'scl:defpackageform, including a sharedcsharp-assembly-utilssupport package),csharp-assembly-utils.lisp(a small runtime-support condition type generated code depends on), andcsharp-assembly-packages.asd(an ASDF system tying the whole batch together), so the output directory is self-contained and loadable with a single(asdf:load-system "csharp-assembly-packages"). All files generated by one invocation share a single creation timestamp.
An --assembly with no --class options is valid — it emits only that assembly's metadata
file, generating no packages, e.g. to inspect an assembly's reflected metadata without
generating any Lisp packages from it.
The metadata is a single Lisp s-expression.
This package was split out of my DotCL Dungeon Slime MonoGame proof of concept.
dotcl-packagegen --out-dir ./cspackages \
--assembly path/to/Some.Assembly.dll \
--class Some.Namespace.Type1 --constant-properties "*" \
--class Some.Namespace.Type2 \
--assembly path/to/Some.Other.Assembly.dll \
--class Some.Other.Namespace.Type3--class attaches to the most recently given --assembly; --constant-properties attaches to
the most recently given --class. --assembly may be repeated to process several assemblies
in one invocation, and a --assembly with no --class options is valid (metadata-only).
--constant-properties (comma/semicolon-separated names, or "*" for all) forces static
read-only properties to be emitted as defconstant instead of define-symbol-macro — safe
only when the property genuinely never changes at runtime (e.g. Vector2.Zero), since
reflection alone can't tell constants from properties that vary.
Nested C# types (a class/struct/enum declared inside another type) are addressed by their
CIL name, which separates nesting levels with + rather than . — e.g.
--class Microsoft.Xna.Framework.Graphics.SpriteFont+Glyph for the Glyph type nested
inside SpriteFont. The generated Lisp package/file name flattens the + the same way it
flattens namespace dots, so that example generates
microsoft-xna-framework-graphics-sprite-font-glyph, not a name containing a literal +.
Assembly files are validated to exist, and requested classes are validated to exist in their
assembly's metadata, before any output file is written; any error is reported in red to
standard error. --version/--help and --test boot the DotCL host
(DotclHost.Initialize()); the metadata-reflection portion of a --out-dir invocation
intentionally does not, since it's pure reflection and runs before DotCL boots.
A Makefile is provided with the following targets:
-
make build— Builds the project (dotnet build dotcl-packagegen.csproj -c Debug), compiling both the C# host and the DotCL Common Lisp sources intobin/Debug/net10.0/. -
make test— Builds the project (viabuild), then runs the built executable in--testmode. This runs the generator's own Lisp unit tests (package-generator-tests.lisp, including the generatedSystem.TimeSpanoperator-overload checks) followed by theAssemblyToLispymetadata test suite againstSystem.Runtime.dll,System.Console.dll, the syntheticAssemblyToLispyTestTarget.dll, andDotCL.Runtime.dll. -
make package— Builds Release binaries for every configuredRuntimeIdentifier(linux-x64,linux-arm64,win-x64,osx-x64,osx-arm64,any) and produces the installable NuGet package(s) (dotnet pack -c Release -o nupkg) in thenupkg/directory: one package per RID plus a top-level meta-package that dispatches to them. -
make deploy— Depends onpackage, then installs (or reinstalls, if already present)dotcl-packagegenas a globaldotnet toolfrom the package(s) just built (dotnet tool install --global --add-source nupkg dotcl-packagegen), making thedotcl-packagegencommand available onPATHfrom any directory. -
make clean— Runsdotnet cleanand removes thebin/,obj/,AssemblyToLispyTestTarget/bin/,AssemblyToLispyTestTarget/obj/, andnupkg/directories.
Typical workflow: make build test while developing, make deploy to install
a local build as the system-wide dotcl-packagegen command.