I ran into this during plugin development and tracked down the cause a while ago with headless tests (
Unity 2018.1.9f2, running VaM's own
Assembly-CSharp.dll and
mcs.dll), and how it could be fixed.
I'll share my findings here since the fix seems quite small, and I've now confirmed it inside VaM itself. Anyone interested can also have a look.
Here is the report I wrote a while ago diagnosing the issue:
The issue
Declaring any
enum in a plugin crashes VaM natively while the plugin compiles. It doesn't need to be used, and
: byte or nested enums crash the same way. Because it's an access violation inside the Mono runtime, nothing catches it: no compiler error, VaM just dies with the unit crash handler. Minimal repro:
C#:
public class EnumRepro : MVRScript { }
public enum Mode { A, B, C } // this line alone is enough
Stack at the crash:
Code:
mono_class_layout_fields mono/metadata/class.c:1637 <- access violation
typebuilder_setup_fields mono/metadata/reflection.c
mono_reflection_create_runtime_class mono/metadata/reflection.c
System.Reflection.Emit.TypeBuilder.CreateType()
Mono.CSharp.TypeDefinition.CloseContainer()
DynamicCSharp.Compiler.McsDriver.Compile(...)
Cause
The embedded mcs builds an enum in this order:
DefineType(name, attrs, null): no parent yet.
DefineField("value__", ...): this is the first field.
SetParent(typeof(System.Enum)).
This Mono version (Unity's legacy fork,
branch unity-2018.1) records an enum's underlying type only once: when the first field is added, and only if the type is already an enum at that point. Here it isn't yet, so the type ends up as an enum with no underlying type. When the runtime then lays out the enum's members, it reads a NULL pointer at
class.c:1637. Structs and classes never take this path.
You can reproduce it without VaM using plain Reflection.Emit on the Unity 2018.1 runtime:
C#:
var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Repro"), AssemblyBuilderAccess.Run);
var mb = ab.DefineDynamicModule("Repro");
var tb = mb.DefineType("Mode", TypeAttributes.Public | TypeAttributes.Sealed, null);
tb.DefineField("value__", typeof(int),
FieldAttributes.Public | FieldAttributes.SpecialName | FieldAttributes.RTSpecialName);
tb.SetParent(typeof(Enum)); // mcs's order: parent AFTER value__
tb.DefineField("A", tb, FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal)
.SetConstant(0);
tb.CreateType(); // native crash here
Move
SetParent(typeof(Enum)) above
DefineField("value__", ...), or pass
typeof(Enum) to
DefineType directly, and you get a working
Int32 enum.
Likely fix
In mcs,
Mono.CSharp.Enum.DoDefineContainer(), swap two adjacent statements so the parent is set before
value__ is defined:
C#:
// current (crashes on this runtime)
TypeBuilder.DefineField(UnderlyingValueField, UnderlyingType.GetMetaInfo(),
FieldAttributes.Public | FieldAttributes.SpecialName | FieldAttributes.RTSpecialName);
DefineBaseTypes(); // -> SetParent(System.Enum)
// fixed
DefineBaseTypes();
TypeBuilder.DefineField(UnderlyingValueField, UnderlyingType.GetMetaInfo(),
FieldAttributes.Public | FieldAttributes.SpecialName | FieldAttributes.RTSpecialName);
In the shipped
VaM_Data/Managed/mcs.dll, that comes down to moving 7 bytes of IL within that one method. Nothing else in the file changes. I tested a patched copy headless through DynamicCSharp's compile-and-load path. Unused, used, byte-backed and nested enums all compiled, loaded and ran, and so did one using explicit values, an enum field,
switch,
ToString and
Enum.Parse. The unpatched
mcs.dll crashed on every one. An equivalent alternative is to pass
System.Enum as the parent to
DefineType when mcs creates an enum's type.
Confirmed in VaM
I swapped the patched
mcs.dll into
VaM_Data/Managed and added this test plugin to an atom:
C#:
using System;
using System.Collections.Generic;
public class HkEnumTest : MVRScript
{
public enum Nested { First, Second }
public HkTestMode Mode = HkTestMode.Low;
HkTestFlags _flags = HkTestFlags.A | HkTestFlags.C;
public override void Init()
{
string sw;
switch (Mode)
{
case HkTestMode.Low: sw = "low"; break;
default: sw = "other"; break;
}
var counts = new Dictionary<HkTestMode, int>();
counts[HkTestMode.High] = 7;
var list = new List<Nested> { Nested.Second };
HkTestMode parsed = (HkTestMode)Enum.Parse(typeof(HkTestMode), "High");
SuperController.LogMessage("enum test OK switch=" + sw + " toString=" + Mode + " cast=" + (int)Mode
+ " flags=" + (byte)_flags + " dict=" + counts[HkTestMode.High]
+ " listContains=" + list.Contains(Nested.Second) + " parse=" + parsed
+ " values=" + Enum.GetValues(typeof(HkTestMode)).Length);
}
}
// Top-level enums go below the MVRScript class.
public enum HkTestMode { Off, Low, High }
public enum HkTestFlags : byte { None = 0, A = 1, B = 2, C = 4 }
It compiled and logged:
Code:
enum test OK switch=low toString=Low cast=1 flags=5 dict=7 listContains=True parse=High values=3
With the stock
mcs.dll, the same plugin crashes VaM at compile.