Skip to content

LuaCATS definitions

xLua can write LuaCATS definition files for the C# API your Lua code calls. Editors that use lua-language-server (LuaLS), such as VS Code with the Lua extension, read them to complete CS.* names, show signatures and report wrong argument types or misspelled members.

In the Unity editor, run XLua > Generate LuaCATS Definitions. The files go to <project>/LuaTypes/, next to Assets/. That folder sits outside Assets, so Unity never imports the files and no xLua loader picks them up as scripts.

Generate again after you change the LuaCallCSharp or ReflectionUse configuration and Unity has compiled it. The generator deletes files it wrote earlier for types that are gone and leaves other files in the folder alone. XLua > Clear and Generate LuaCATS Definitions first deletes every file the generator wrote in the folder and its subfolders, then generates. It recognizes them by their first two lines, ---@meta and the -- Generated by xLua LuaCATS generator marker; other files stay.

The generator loads the native xLua plugin and asks it which Lua it runs. With Lua 5.3 or later, C# integral types become integer; with LuaJIT they become number, except long and ulong (see below).

Outside Unity, XLuaGenerate.exe (built by Tools/build_tools.py) takes --luacats and the options below before its usual arguments:

Terminal window
mono Tools/XLuaGenerate.exe --luacats [--strict] [--suffix .d.lua] [--luarc path | --no-luarc] [--clear] \
Game.dll Game.Editor.dll LuaTypes/ [search paths...]

The output does not depend on the machine: files are UTF-8 without BOM with \n line ends, members are sorted, and the current culture is not used.

The definitions come from the xLua source generator (Configuration). It writes a binding manifest into every assembly it compiles (XLua.Manifest.g.cs, compiled only in the Editor and with XLUA_GENERAL; see Custom code generation), and the definitions for that assembly come from it:

  • the types it wraps, with exactly the members their wrappers bind, so the definitions and the bindings cannot disagree;
  • its [ReflectionUse] types and its BlackList entries by type, which also hide members of the base classes declared in full;
  • the extension methods of its extension method map, for types Lua reaches through reflection;
  • DoNotGen members, and the members the generator leaves to reflection (info XLSG004, such as init setters), which xLua still reaches.

An assembly without a manifest declares nothing: configuration lists are not read, and an assembly that the generator did not compile has no bindings to describe. The console says which assemblies came from a manifest, and a run that finds none says that only xLua’s own API is declared. Generate the definitions again after the assemblies recompiled.

Each option is a field of CSObjectWrapEditor.GeneratorConfig. In Unity, set it by marking a static field or property of a static editor class with the attribute, as for GenPath (Configuration):

Attribute (type) Default XLuaGenerate Effect
LuaCATSPath (string) <project>/LuaTypes/ output path argument Output folder.
LuaCATSFileSuffix (string) .lua --suffix Ending of the file names, such as .d.lua. It must end in .lua, the only files LuaLS reads. xlua/util.lua keeps its name so that require 'xlua.util' resolves.
LuaCATSStrict (bool) false --strict Strict CS names, see below.
LuaCATSLuarcPath (string) .luarc.json in the folder that holds the output folder --luarc, --no-luarc The .luarc.json to write; an empty string turns it off.
public static class LuaCATSConfig
{
[CSObjectWrapEditor.LuaCATSPath]
public static string Path = System.IO.Path.GetFullPath(UnityEngine.Application.dataPath + "/../Tools/LuaTypes");
[CSObjectWrapEditor.LuaCATSFileSuffix]
public static string Suffix = ".d.lua";
[CSObjectWrapEditor.LuaCATSStrict]
public static bool Strict = true;
[CSObjectWrapEditor.LuaCATSLuarcPath]
public static string Luarc = ""; // keep my own .luarc.json
}

The generator writes a .luarc.json next to the output folder, by default in the Unity project folder, with the two settings LuaLS needs:

{
"runtime.version": "Lua 5.5",
"workspace.library": [
"LuaTypes"
]
}

runtime.version is Lua 5.5, Lua 5.4, Lua 5.3 or LuaJIT, after the plugin; Lua 5.5 needs LuaLS 3.16 or later. The library path is relative to the folder of the .luarc.json, which LuaLS resolves against the workspace root, so open that folder in the editor.

The generator never overwrites a .luarc.json you wrote. JSON has no comments, so it recognizes its own file by its exact content: those two settings and nothing else. Once you add a setting, the file is yours, and the generator only prints a hint when runtime.version or the library entry is missing. It also writes nothing when a .luarc.jsonc is in the same folder, because LuaLS then reads only one of the two files.

LuaLS treats library files as read-only definitions; it does not report problems in them. To check scripts from the command line, run lua-language-server --check=<folder> --checklevel=Warning.

By default, CS and every namespace table carry ---@field [string] any: xLua reaches any C# type through reflection, so CS.System.Console is valid at run time even when it is not configured, and LuaLS types it any.

With LuaCATSStrict, those fallbacks are left out. LuaLS then reports every CS name that is not a configured type, one of its base classes or a namespace of them, as undefined-field, which catches misspelled type names. Names that work through reflection are reported too, as are generic type definitions such as CS.System.Collections.Generic.List(CS.System.Int32). Configure the types you use, or silence a line with ---@diagnostic disable-next-line: undefined-field.

XLua > Validate Setup reports configuration problems as warnings in the console and changes nothing. Scripts and tests call CSObjectWrapEditor.SetupValidator.Validate(), which returns the messages, and XLuaGenerate.exe --validate prints them and exits with 1. It checks:

  • the native plugin loads and its xlua_get_lib_version matches the version the C# code expects;
  • every copy of XLua.SourceGenerator.dll has the RoslynAnalyzer label (Unity only);
  • every assembly with xLua configuration ([LuaCallCSharp], [CSharpCallLua], [GCOptimize] or [ReflectionUse] on a type or at assembly level) has generated bindings. The message gives the likely cause: the source generator is not installed (with the command that installs it), or it did not run for that assembly (warning CS9057, a lost RoslynAnalyzer label, an assembly that does not reference XLua);
  • the GenPath folder holds no output of the removed Generate Code (XLuaGenAutoRegister.cs, DelegatesGensBridge.cs or WrapPusher.cs), which no longer compiles;
  • asmdefs under Assets whose scripts use xLua reference XLua, and XLua.Core too when the source generator is installed and they hold configuration (Unity only);
  • the LuaCATS output folder can be created and written (it creates and deletes a temporary file), the file suffix ends in .lua, and an existing .luarc.json you wrote has the settings for the plugin;
  • with HOTFIX_ENABLE, Tools/XLuaHotfixInject.exe and Mono.Cecil.dll exist;
  • no Resources folder under Assets or Packages holds a Lua module that earlier xLua versions shipped in Assets/XLua/Resources and an in-place upgrade left behind: xlua/util.lua.* and perf/profiler.lua.*, which would replace the modules now embedded in xLua because the Resources searcher runs first, and perf/memory.lua.* and tdr/tdr.lua.*, whose modules were removed (see MIGRATING.md). The warning says whether the file is unchanged from a version xLua shipped (delete it and its .meta) or differs (keep it only if you edited it on purpose). Folders Unity does not import (names starting with . or ending in ~) are skipped. With XLUA_NO_UTIL a Resources copy of xlua.util is how the module is provided, so it is not reported. XLuaGenerate.exe --validate checks this only for the folders given with --resources-root (repeatable);
  • every configuration list (a static field or property with a configuration attribute) is reported once: the source generator does not read it, so convert it with XLua > Migrate Configuration to Attributes and delete it (Converting configuration lists);
  • the entries of BlackList lists, which the Editor’s reflection wrappers still read, name a type that exists and a member, or an overload, that the type declares. These entries are strings, so after a rename they silently stop working. For the attribute forms the compiler reports such entries as warning XLSG003, with file and line.

The configuration checks run only when the plugin passed, since reading the configuration starts a Lua state.

The definitions declare the types the manifests list: every type the source generator wraps (LuaCallCSharp), with the members its manifest lists, and every ReflectionUse type, with the BlackList and [Obsolete] filtering. They add the base classes of those types in full, since Lua reaches their members through every derived object. Members listed in DoNotGen stay, because xLua still reaches them through reflection.

File Contents
<Namespace>.<Type>.lua One file per C# type.
CS.lua The CS root and namespace tables, delegate aliases, and empty classes for types that only appear in signatures.
xlua.lua xlua.* helpers, typeof, cast, base, uint64, the array type and, on LuaJIT, xlua.Int64 (and xlua.UInt64).
xlua/util.lua The module returned by require 'xlua.util'.

One file per type keeps each file well under the 500 KB LuaLS workspace.preloadFileSize limit, even for large namespaces such as UnityEngine.

C# LuaCATS
bool boolean
sbyte, byte, short, ushort, int, uint, char integer (Lua 5.3+) or number (LuaJIT)
long, ulong integer (Lua 5.3+) or xlua.Int64 (LuaJIT; parameters take number|xlua.Int64); ulong is xlua.UInt64 on LuaJIT built with UINT_ESPECIALLY
float, double number
string, byte[] string
object any
void return no @return
T? (Nullable<T>) T?
LuaTable, LuaFunction table, function
IntPtr lightuserdata
class, struct, interface CS.Namespace.Type; parents are the base class and the implemented interfaces that appear in the files
nested type Outer.Inner CS.Namespace.Outer.Inner
constructed generic, such as List<int> CS.System.Collections.Generic.List_System_Int32
T[] xlua.Array<T>: zero-based [integer] access and Length
delegate ---@alias CS.Namespace.Delegate fun(...)
enum a class whose values are fields of the enum type, plus __CastFrom

A parameter that xLua converts from a Lua table also accepts table: structs, classes with a public parameterless constructor, interfaces and arrays (T[]).

LuaJIT has no 64-bit integers, so xLua passes long and ulong to Lua as a userdata, declared as the class xlua.Int64, and takes either that userdata or a number back. Writable long fields and properties are typed number|xlua.Int64, since LuaCATS has one type for reads and writes. +, -, *, /, %, ^, unary -, tostring and .. work with a number on either side (.. also with a string). ==, < and <= only compare two xlua.Int64 values: x == 0 is always false and x < 1 raises an error, and LuaLS reports neither (see the 64-bit integer section of the tutorial for the alternatives). The LuaJIT output also declares the uint64 helpers that only LuaJIT builds have: band, bor, bxor, bnot, lshift, rshift, arshift and idiv. Values that Lua supplies take a number too: indexer keys, the operand of a C# operator, and what a Lua function returns for a delegate. A delegate alias describes the Lua function, so its parameters are xlua.Int64; when Lua calls a C# delegate, a long result is typed number|xlua.Int64.

A LuaJIT plugin built with -DUINT_ESPECIALLY=ON (default off) pushes ulong as a separate unsigned kind of the userdata. The generator detects this through the plugin (tostring(uint64.parse('1')) is 1U) and declares ulong as xlua.UInt64, with parameters taking number|xlua.UInt64; uint64.parse, uint64.divide and uint64.remainder return it, and the LuaJIT-only helpers have overloads that return it for an xlua.UInt64 operand. It has the same operators, unsigned, but unary - returns an xlua.Int64, tostring appends U, and a number of 2^63 or more saturates to 2^63 - 1 before use. Any operator with one xlua.Int64 and one xlua.UInt64 raises “type not match”, which LuaLS does not report; it does report passing one where the other is declared. xLua’s argument checks reject the other kind, but fields, single-overload methods and array tables convert it without a check and reinterpret the bits.

Members follow the Lua call syntax:

  • Fields and properties are ---@field entries, marked static or read-only in their description. A name that is not a Lua identifier, such as a keyword or a name with non-ASCII letters, is quoted: ---@field ['Şehir'] integer, which matches the obj['Şehir'] that Lua code has to use.
  • Instance methods use : and static methods use .. Overloads become ---@overload entries in xLua’s resolution order. Optional parameters end in ?, and params T[] becomes ...: T.
  • out parameters leave the argument list. The return value comes first, followed by every out and ref parameter in order, as xLua returns them.
  • Constructors are ---@overload fun(...): CS.Type on the class, so CS.UnityEngine.GameObject('name') type-checks. Structs also get fun().
  • An event is a method that takes "+" or "-" and a handler: obj:Clicked('+', handler).
  • Indexers with a non-string key become ---@field [integer] T, and +, -, *, /, % and the bitwise operators become ---@operator entries.
  • Extension methods from configured static classes appear as instance methods of the type they extend.
  • Generic methods appear only when xLua can call them: every type parameter is constrained to a class and appears as a parameter type. xLua calls them with each type argument set to its first constraint, not inferred from the arguments, so T Clone<T>(T source) where T : Shape, new() runs as Clone<Shape> and may return a Shape for a Circle. The signature therefore uses the constraint type (CS.Shape), not ---@generic T, which would claim the result has the argument’s type.
  • LuaCATS has one class per name, so instances also show static members, and the class table also shows instance members and the call signature.
  • By default the namespace tables accept any key ([string] any). xLua reaches every C# type through reflection, so CS.System.Console is valid even when it is not configured. A misspelled type name is therefore not reported, and LuaLS also skips undefined-member checks on a chain read straight through a namespace, such as CS.Game.Color.Purple. Keep the class in a local (local Color = CS.Game.Color) and Color.Purple is reported, or turn on strict CS names.
  • LuaLS picks an overload by argument count before it checks argument types. When two C# overloads take the same number of arguments, or none takes the number you pass, it reports nothing.
  • Helper parameters that take a C# class table, such as the one for xlua.private_accessible, have type any, so LuaLS does not report a missing argument there.
  • Generic type definitions are not declared. CS.System.Collections.Generic.List(CS.System.Int32) has type any; annotate the result with ---@type to get the generated class.
  • Operators whose left operand is not the declaring type, such as 2 * vector, and comparison operators have no LuaCATS form.
  • Types that appear only in signatures are empty classes, so their members are reported as undefined. Add those types to LuaCallCSharp or ReflectionUse to declare them.
  • LuaCATS has no read-only fields; assigning a read-only property is not reported.
  • xlua.cast(obj, typeof(T)) changes the object in place and returns nothing. Use ---@cast obj CS.T to tell the editor about the new type.