Skip to main content

Features & usage comparison (xLua / toLua / SLua / ZLua)

Nature: evaluation material, not a ZLua behavior Spec. ZLua status: Mono (Editor) and Il2Cpp (Player) are both done (see impl/MONO.md).


1. Overview

DimensionxLuatoLua / tolua#SLuaZLua
Lua engineSeparate libxlua (P/Invoke)Embedded or bound native luaEmbedded luaLinked into libil2cpp (Player) / embedded (Editor)
Type entryCS.Namespace.TypeNamespace.Type (BeginModule chain)Like toLua + configCSharp[assembly]['Full.Name'] lazy load
Lua→C# bridgeGenerated C# Wrap + LuaDLLGenerated *.Wrap.csAuto-bind + exportC++ MethodBridge (Il2Cpp) / Expression Emit (Mono)
C#→LuaLuaEnv + LuaFunction + many LuaDLLLuaState / LuaFunctionLuaSvr / LuaFunctionGetFunction<T> + Delegate bridge
Whitelist / export[LuaCallCSharp] / [CSharpCallLua] + GenerateManual lists / BinderExport config / AttributeNo LuaCall whitelist; by public + lazy Bind
Editor vs PlayerMostly same (libxlua + Wrap)Mostly sameMostly sameDual track: Mono Emit vs Il2Cpp native (semantics must match)
Unity invasivenessPlugin + nativePluginPluginfork libil2cpp (Player)
EventDedicated supportDepends on versionDepends on versionNone; use ordinary add_ / remove_ methods
Docs / communityStrongWeak (stale risk)WeakBuilding

2. Type access

2.1 Syntax (same type MyGame.Demo)

SolutionTypical syntax
xLuaCS.MyGame.Demo
toLuaUnityEngine.GameObject (BeginModule namespace chain; not only global short names)
SLuaUnityEngine.GameObject (auto-exported namespaces)
ZLuaCSharp['Assembly-CSharp']['MyGame.Demo'] or CSharp.AC['MyGame.Demo']

ZLua rule highlights:

  • Types with namespaces must use a bracket key for the full typeFullName; CSharp.AC.MyGame.Demo is forbidden (. is not a table path).
  • Nested types use +: CSharp.AC['Outer+Inner'].
  • Assembly names are simple names: Assembly-CSharp, mscorlib.

See spec/02-TYPE-SYSTEM.md §2.

2.2 Lazy load vs pre-export

SolutionModelPackage / link impact
xLuaGenerate whitelisted types → Wrap in packageUnexported types unreachable; size controllable
toLua / SLuaExport list decides Wrap countMore exports → more generated code
ZLuaFirst CSharp[asm][type] access triggers EnsureBindingRuntime bind + Il2Cpp stub tables; unaccessed types take no bridge table slots (link still keeps metadata)

2.3 Generics & arrays

CapabilityxLuatoLuaSLuaZLua
Closed genericsCS.System.Collections.Generic.List(CS.System.Int32) etc.Pre-export or reflectionConfig exportzlua.make_generic_type(base, ...)
Array typesExport or reflectionExportExportzlua.make_szarray_type / make_mdarray_type
Runtime array constructionSupported (depends on export)LimitedLimitedzlua.new_szarray_by_element_type etc.

3. Member calls (Lua→C#)

3.1 Static / instance

Shared example: static Demo.Add(1, 2), instance obj:GetX().

SolutionStaticInstance
xLuaCS.Demo.Add(1, 2)obj:GetX()
toLuaDemo.Add(1, 2)obj:GetX()
SLuaSame as toLuaSame as toLua
ZLuaDemo.Add(1, 2) (Demo is type table)obj:GetX()

ZLua separates static/instance into three tables (method / fieldGetter / fieldSetter); inherited members are flattened at Bind time — no runtime walk up the inheritance chain.

3.2 Fields & properties

SolutionRead fieldWrite read-only property
xLuaOften via Wrap / propertyWrap errors
toLua / SLuaWrap or getterSame
ZLuaobj.x → fieldGetter table; Il2Cpp can read by offset__newindex miss → error

3.3 Method overloads

SolutionStrategy
xLuaOverload dispatch inside generated Wrap
toLua / SLuaDispatch inside Wrap or single signature
ZLuaRegistered at Bind; default best match; [LuaAlias] / register_method for explicit binds (see spec/04-METHOD-OVERLOAD.md)

ZLua-specific capabilities:

-- Bind-time [LuaAlias("foo_str")] single-candidate direct closure
obj:foo_str("a")

-- Or attach a new name at runtime (must be free)
local run = demo.run_i32
zlua.register_method("run_hot", run)
demo:run_hot(1)

3.4 __index miss semantics

SolutionMissing member
xLuaUsually nil or error (depends on Wrap)
toLua / SLuaOften error
ZLuanil (read); write unknown key → error

4. C#→Lua

4.1 Entry points

SolutionC# calls Lua functionLua function → C# delegate
xLuaLuaEnv.DoString / LuaFunction.Call / [CSharpCallLua]LuaFunction / Delegate bridge
toLuaLuaState.DoFile / LuaFunctionLuaFunction.ToDelegate etc.
SLuaLuaSvr + LuaFunctionSLua delegate binding
ZLuaLuaAppDomain.GetFunction<Action<float>>("game", "OnTick")Method params implicit marshal (Action/Func etc.)

ZLua GetFunction example:

static readonly Action<float> OnTick =
LuaAppDomain.GetFunction<Action<float>>("game", "OnTick");
OnTick(0.016f);
  • Editor / Player: same API; runtime calls Lua via Delegate bridge (cache the delegate on hot paths).

4.2 Module loading

SolutionLoading
xLuarequire + custom loader
toLua / SLuaCustom loader
ZLuaLuaAppDomain.Initialize(moduleLoader); integrates with require (see spec/01-HOST-API.md)

5. Value types, ref, struct

TopicxLuatoLua / SLuaZLua
struct argsOften boxing or tableDepends on WrapByVal userdata copy / ByObj boxed
struct returnsOften allocateSameByVal payload or boxed (see spec/marshal/05-STRUCT.md)
ref/out Lua→C#Multi-return or tableMulti-returnStructUserData (Type(...) / C# push) or copy semantics
C#→Lua ref/outDepends on versionLimitedOpaqueValue (valid only for that call frame)
enumnumber / exported typeExportinteger default; optional ByObj boxed
zlua.castDeclared-type facade conversion

Opaque boundaries (ZLua-specific; easy migration pitfalls):

  • On a C# GetFunction delegate, ref int pushed to Lua is OpaqueValue, not integer; use zlua.get_opaquevalue / set_opaquevalue.
  • Opaque must not persist across pcall.

6. Hotupdate, codegen & stripping

DimensionxLuatoLua / SLuaZLua
Hotupdate practiceMany ready solutions (bytecode, assets)Project-builtBuild your own; ZLua does not bind a specific hotupdate framework
CodegenXLua Generate AllExport WrapIl2Cpp: Codegen C++ stubs (Lua→C#); Mono: Emit (not in Player package); C#→Lua no codegen
Reflection fallbackYes (slow path)PartialForbids silent hot-path Method.Invoke; Emit failure → bind-time failure
Link / stripWhitelist controls WrapExport listPublic types can lazy Bind; Il2Cpp ReducedType controls stub size (see BRIDGE.md)
Unity upgradeMostly bump xLua packageHigh riskMerge libil2cpp patches (engineering debt)

7. Editor / Player consistency

SolutionDual ends
xLua / toLua / SLuaUsually same lib + Wrap; Editor ≈ Player
ZLuaMono and Il2Cpp must share Lua-visible semantics; implementations differ (Emit vs C++ stubs)

Testing requirement: run the same cases once in Editor and once on Il2Cpp Player; any failure fails (see guides/TESTING.md).

Indexer properties / open generics etc.: see compatibility matrix and Spec; dual-end semantics match; limited items are limited on both ends.


8. Invasiveness & maintenance

Shallow ←────────────────────────────────────────→ Deep (Il2Cpp invasiveness)

Pure C# reflection bridge
xLua / toLua / SLua (plugin + native / Wrap)
★ ZLua Player target (embed libil2cpp)
HybridCLR-level VM changes (ZLua does not do this)
LayerxLuatoLua / SLuaZLua
Modify libil2cppNoNoYes (Player)
Separate nativelibxluaOptionalNo (same binary as il2cpp)
GC hooksUsually noneUsually nonenon-blittable struct etc. may hook push_other_roots
Maintenance focusPackage versionsStale riskUnity versions + zlua patch merges

9. Config & whitelist

SolutionMechanism
xLua[LuaCallCSharp], [CSharpCallLua], [ReflectionUse], Generate config
toLuaCustom CustomSettings.cs export list
SLua[CustomLuaClass], export XML / code
ZLuaNo LuaCall-style whitelist; public members can Bind; [LuaMarshalAs] / [LuaAlias] affect Marshal and aliases

Migration meaning: leaving xLua means deleting Generate config and confirming which public APIs in assemblies should be exposed to Lua; sensitive APIs should become non-public, not rely on an export list.


10. Unsupported or weak items (migration checklist)

ItemxLuatoLua / SLuaZLua
C# Event sugarYesDepends on versionNoneadd_Xxx / remove_Xxx
Runtime inheritance lookupYesYesNone (Bind-time flattening)
CS. / UnityEngine.* etc.YesYes (different shapes)Native uses CSharp; migration period can use adaptors
Hot-path reflection InvokeFallbackPartialExplicit error
Cross-frame OpaqueN/AN/AForbidden
Arbitrary Lua function as permanent delegate without GC concernsWatch translatorWatch carefullyMust understand spec/10-LIFETIME.md

11. Same example, four columns

Need: call MyGame.Demo.Add(1, 2), create instance, read field x.

-- xLua
local Demo = CS.MyGame.Demo
local sum = Demo.Add(1, 2)
local obj = Demo()
local x = obj.x

-- toLua (Demo already exported globally)
local sum = Demo.Add(1, 2)
local obj = Demo.New()
local x = obj.x

-- SLua
local Demo = MyGame.Demo
local sum = Demo.Add(1, 2)
local obj = Demo()
local x = obj.x

-- ZLua (native)
local Demo = CSharp['Assembly-CSharp']['MyGame.Demo']
local sum = Demo.Add(1, 2)
local obj = Demo()
local x = obj.x

-- ZLua + xlua adaptor (migration transition; CS.* still works on the checklist)
-- after require + adaptor.init(export_types):
-- local Demo = CS.MyGame.Demo

Type-path adaptor details: guides/migration and spec/12-MIGRATION-ADAPTORS.


12. Selection summary

Better fitSolution
Ship now, fewer pitfalls, team already has xLua assetsxLua
Legacy toLua/SLua already stable, small change surfaceKeep current (migrating to ZLua is costly)
Player performance boundary is the bottleneck; willing to maintain libil2cpp; want C#-aligned semanticsZLua
Unwilling to change engine layer; don’t need extreme interop perfxLua over ZLua

Migration steps: guides/migration/.


DocContent
PERFORMANCE.mdPerformance comparison (zlua-benchmark)
GC.mdGC comparison
spec/02-TYPE-SYSTEM.mdZLua type system Spec