Skip to content

Commit b53926c

Browse files
authored
fix: Fix NetworkVariable<INetworkSerializable> so that it will properly call NetworkSerialize instead of using the default memcpy operation. (#1383)
* fix: Fix NetworkVariable<INetworkSerializable> so that it will properly call NetworkSerialize instead of using the default memcpy operation. * Changelog entry * standards fix * Whoops, syntax error. * More standards fixes * Fixed typo * Moved changelog entry to Unreleased
1 parent 33b589e commit b53926c

File tree

10 files changed

+325
-4
lines changed

10 files changed

+325
-4
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
2222
- Fixed invalid IL code being generated when using `this` instead of `this ref` for the FastBufferReader/FastBufferWriter parameter of an extension method. (#1393)
2323
- Fixed an issue where if you are running as a server (not host) the LoadEventCompleted and UnloadEventCompleted events would fire early by the NetworkSceneManager (#1379)
2424
- Fixed a runtime error when sending an array of an INetworkSerializable type that's implemented as a struct (#1402)
25+
- Fixed NetworkVariable not calling NetworkSerialize on INetworkSerializable types (#1383)
2526

2627
### Changed
2728

com.unity.netcode.gameobjects/Editor/CodeGen/CodeGenHelpers.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,28 @@ public static void AddError(this List<DiagnosticMessage> diagnostics, SequencePo
264264
});
265265
}
266266

267+
public static void AddWarning(this List<DiagnosticMessage> diagnostics, string message)
268+
{
269+
diagnostics.AddWarning((SequencePoint)null, message);
270+
}
271+
272+
public static void AddWarning(this List<DiagnosticMessage> diagnostics, MethodDefinition methodDefinition, string message)
273+
{
274+
diagnostics.AddWarning(methodDefinition.DebugInformation.SequencePoints.FirstOrDefault(), message);
275+
}
276+
277+
public static void AddWarning(this List<DiagnosticMessage> diagnostics, SequencePoint sequencePoint, string message)
278+
{
279+
diagnostics.Add(new DiagnosticMessage
280+
{
281+
DiagnosticType = DiagnosticType.Warning,
282+
File = sequencePoint?.Document.Url.Replace($"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}", ""),
283+
Line = sequencePoint?.StartLine ?? 0,
284+
Column = sequencePoint?.StartColumn ?? 0,
285+
MessageData = $" - {message}"
286+
});
287+
}
288+
267289
public static void RemoveRecursiveReferences(this ModuleDefinition moduleDefinition)
268290
{
269291
// Weird behavior from Cecil: When importing a reference to a specific implementation of a generic
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Collections.Generic;
5+
using System.Reflection;
6+
using Mono.Cecil;
7+
using Mono.Cecil.Cil;
8+
using Mono.Cecil.Rocks;
9+
using Unity.CompilationPipeline.Common.Diagnostics;
10+
using Unity.CompilationPipeline.Common.ILPostProcessing;
11+
using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor;
12+
using MethodAttributes = Mono.Cecil.MethodAttributes;
13+
14+
namespace Unity.Netcode.Editor.CodeGen
15+
{
16+
17+
internal sealed class INetworkSerializableILPP : ILPPInterface
18+
{
19+
public override ILPPInterface GetInstance() => this;
20+
21+
public override bool WillProcess(ICompiledAssembly compiledAssembly) =>
22+
compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName ||
23+
compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == CodeGenHelpers.RuntimeAssemblyName);
24+
25+
private readonly List<DiagnosticMessage> m_Diagnostics = new List<DiagnosticMessage>();
26+
27+
public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
28+
{
29+
if (!WillProcess(compiledAssembly))
30+
{
31+
return null;
32+
}
33+
34+
35+
m_Diagnostics.Clear();
36+
37+
// read
38+
var assemblyDefinition = CodeGenHelpers.AssemblyDefinitionFor(compiledAssembly, out var resolver);
39+
if (assemblyDefinition == null)
40+
{
41+
m_Diagnostics.AddError($"Cannot read assembly definition: {compiledAssembly.Name}");
42+
return null;
43+
}
44+
45+
// process
46+
var mainModule = assemblyDefinition.MainModule;
47+
if (mainModule != null)
48+
{
49+
try
50+
{
51+
if (ImportReferences(mainModule))
52+
{
53+
var types = mainModule.GetTypes()
54+
.Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializable_FullName) && !t.Resolve().IsAbstract && t.Resolve().IsValueType)
55+
.ToList();
56+
// process `INetworkMessage` types
57+
if (types.Count == 0)
58+
{
59+
return null;
60+
}
61+
62+
CreateModuleInitializer(assemblyDefinition, types);
63+
}
64+
else
65+
{
66+
m_Diagnostics.AddError($"Cannot import references into main module: {mainModule.Name}");
67+
}
68+
}
69+
catch (Exception e)
70+
{
71+
m_Diagnostics.AddError((e.ToString() + e.StackTrace.ToString()).Replace("\n", "|").Replace("\r", "|"));
72+
}
73+
}
74+
else
75+
{
76+
m_Diagnostics.AddError($"Cannot get main module from assembly definition: {compiledAssembly.Name}");
77+
}
78+
79+
mainModule.RemoveRecursiveReferences();
80+
81+
// write
82+
var pe = new MemoryStream();
83+
var pdb = new MemoryStream();
84+
85+
var writerParameters = new WriterParameters
86+
{
87+
SymbolWriterProvider = new PortablePdbWriterProvider(),
88+
SymbolStream = pdb,
89+
WriteSymbols = true
90+
};
91+
92+
assemblyDefinition.Write(pe, writerParameters);
93+
94+
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics);
95+
}
96+
97+
private MethodReference m_InitializeDelegates_MethodRef;
98+
99+
private const string k_InitializeMethodName = nameof(NetworkVariableHelper.InitializeDelegates);
100+
101+
private bool ImportReferences(ModuleDefinition moduleDefinition)
102+
{
103+
104+
var helperType = typeof(NetworkVariableHelper);
105+
foreach (var methodInfo in helperType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public))
106+
{
107+
switch (methodInfo.Name)
108+
{
109+
case k_InitializeMethodName:
110+
m_InitializeDelegates_MethodRef = moduleDefinition.ImportReference(methodInfo);
111+
break;
112+
}
113+
}
114+
return true;
115+
}
116+
117+
private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinition)
118+
{
119+
var staticCtorMethodDef = typeDefinition.GetStaticConstructor();
120+
if (staticCtorMethodDef == null)
121+
{
122+
staticCtorMethodDef = new MethodDefinition(
123+
".cctor", // Static Constructor (constant-constructor)
124+
MethodAttributes.HideBySig |
125+
MethodAttributes.SpecialName |
126+
MethodAttributes.RTSpecialName |
127+
MethodAttributes.Static,
128+
typeDefinition.Module.TypeSystem.Void);
129+
staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
130+
typeDefinition.Methods.Add(staticCtorMethodDef);
131+
}
132+
133+
return staticCtorMethodDef;
134+
}
135+
136+
// Creates a static module constructor (which is executed when the module is loaded) that registers all the
137+
// message types in the assembly with MessagingSystem.
138+
// This is the same behavior as annotating a static method with [ModuleInitializer] in standardized
139+
// C# (that attribute doesn't exist in Unity, but the static module constructor still works)
140+
// https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-5.0
141+
// https://web.archive.org/web/20100212140402/http://blogs.msdn.com/junfeng/archive/2005/11/19/494914.aspx
142+
private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeDefinition> networkSerializableTypes)
143+
{
144+
foreach (var typeDefinition in assembly.MainModule.Types)
145+
{
146+
if (typeDefinition.FullName == "<Module>")
147+
{
148+
var staticCtorMethodDef = GetOrCreateStaticConstructor(typeDefinition);
149+
150+
var processor = staticCtorMethodDef.Body.GetILProcessor();
151+
152+
var instructions = new List<Instruction>();
153+
154+
foreach (var type in networkSerializableTypes)
155+
{
156+
var method = new GenericInstanceMethod(m_InitializeDelegates_MethodRef);
157+
method.GenericArguments.Add(type);
158+
instructions.Add(processor.Create(OpCodes.Call, method));
159+
}
160+
161+
instructions.ForEach(instruction => processor.Body.Instructions.Insert(processor.Body.Instructions.Count - 1, instruction));
162+
break;
163+
}
164+
}
165+
}
166+
}
167+
}

com.unity.netcode.gameobjects/Editor/CodeGen/INetworkSerializableILPP.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
5252
case nameof(NetworkBehaviour):
5353
ProcessNetworkBehaviour(typeDefinition);
5454
break;
55+
case nameof(NetworkVariableHelper):
56+
ProcessNetworkVariableHelper(typeDefinition);
57+
break;
5558
case nameof(__RpcParams):
5659
typeDefinition.IsPublic = true;
5760
break;
@@ -100,6 +103,17 @@ private void ProcessNetworkManager(TypeDefinition typeDefinition, string[] assem
100103
}
101104
}
102105

106+
private void ProcessNetworkVariableHelper(TypeDefinition typeDefinition)
107+
{
108+
foreach (var methodDefinition in typeDefinition.Methods)
109+
{
110+
if (methodDefinition.Name == nameof(NetworkVariableHelper.InitializeDelegates))
111+
{
112+
methodDefinition.IsPublic = true;
113+
}
114+
}
115+
}
116+
103117
private void ProcessNetworkBehaviour(TypeDefinition typeDefinition)
104118
{
105119
foreach (var nestedType in typeDefinition.NestedTypes)

com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariable.cs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,55 @@ namespace Unity.Netcode
99
[Serializable]
1010
public class NetworkVariable<T> : NetworkVariableBase where T : unmanaged
1111
{
12+
// Functions that know how to serialize INetworkSerializable
13+
internal static void WriteNetworkSerializable<TForMethod>(FastBufferWriter writer, ref TForMethod value)
14+
where TForMethod : INetworkSerializable, new()
15+
{
16+
writer.WriteNetworkSerializable(value);
17+
}
18+
internal static void ReadNetworkSerializable<TForMethod>(FastBufferReader reader, out TForMethod value)
19+
where TForMethod : INetworkSerializable, new()
20+
{
21+
reader.ReadNetworkSerializable(out value);
22+
}
23+
24+
// Functions that serialize other types
25+
private static void WriteValue<TForMethod>(FastBufferWriter writer, ref TForMethod value) where TForMethod : unmanaged
26+
{
27+
writer.WriteValueSafe(value);
28+
}
29+
30+
private static void ReadValue<TForMethod>(FastBufferReader reader, out TForMethod value)
31+
where TForMethod : unmanaged
32+
{
33+
reader.ReadValueSafe(out value);
34+
}
35+
36+
internal delegate void WriteDelegate<TForMethod>(FastBufferWriter writer, ref TForMethod value);
37+
38+
internal delegate void ReadDelegate<TForMethod>(FastBufferReader reader, out TForMethod value);
39+
40+
// These static delegates provide the right implementation for writing and reading a particular network variable
41+
// type.
42+
//
43+
// For most types, these default to WriteValue() and ReadValue(), which perform simple memcpy operations.
44+
//
45+
// INetworkSerializableILPP will generate startup code that will set it to WriteNetworkSerializable()
46+
// and ReadNetworkSerializable() for INetworkSerializable types, which will call NetworkSerialize().
47+
//
48+
// In the future we may be able to use this to provide packing implementations for floats and integers to
49+
// optimize bandwidth usage.
50+
//
51+
// The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this,
52+
// NetworkVariable would need to do a `var is INetworkSerializable` check, and then cast to INetworkSerializable,
53+
// *both* of which would cause a boxing allocation. Alternatively, NetworkVariable could have been split into
54+
// NetworkVariable and NetworkSerializableVariable or something like that, which would have caused a poor
55+
// user experience and an API that's easier to get wrong than right. This is a bit ugly on the implementation
56+
// side, but it gets the best achievable user experience and performance.
57+
internal static WriteDelegate<T> Write = WriteValue;
58+
internal static ReadDelegate<T> Read = ReadValue;
59+
60+
1261
/// <summary>
1362
/// Delegate type for value changed event
1463
/// </summary>
@@ -106,7 +155,7 @@ public override void WriteDelta(FastBufferWriter writer)
106155
public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta)
107156
{
108157
T previousValue = m_InternalValue;
109-
reader.ReadValueSafe(out m_InternalValue);
158+
Read(reader, out m_InternalValue);
110159

111160
if (keepDirtyDelta)
112161
{
@@ -119,13 +168,13 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta)
119168
/// <inheritdoc />
120169
public override void ReadField(FastBufferReader reader)
121170
{
122-
reader.ReadValueSafe(out m_InternalValue);
171+
Read(reader, out m_InternalValue);
123172
}
124173

125174
/// <inheritdoc />
126175
public override void WriteField(FastBufferWriter writer)
127176
{
128-
writer.WriteValueSafe(m_InternalValue);
177+
Write(writer, ref m_InternalValue);
129178
}
130179
}
131180
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Unity.Netcode
2+
{
3+
public class NetworkVariableHelper
4+
{
5+
// This is called by ILPP during module initialization for all unmanaged INetworkSerializable types
6+
// This sets up NetworkVariable so that it properly calls NetworkSerialize() when wrapping an INetworkSerializable value
7+
//
8+
// The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this,
9+
// NetworkVariable would need to do a `var is INetworkSerializable` check, and then cast to INetworkSerializable,
10+
// *both* of which would cause a boxing allocation. Alternatively, NetworkVariable could have been split into
11+
// NetworkVariable and NetworkSerializableVariable or something like that, which would have caused a poor
12+
// user experience and an API that's easier to get wrong than right. This is a bit ugly on the implementation
13+
// side, but it gets the best achievable user experience and performance.
14+
//
15+
// RuntimeAccessModifiersILPP will make this `public`
16+
internal static void InitializeDelegates<T>() where T : unmanaged, INetworkSerializable
17+
{
18+
NetworkVariable<T>.Write = NetworkVariable<T>.WriteNetworkSerializable;
19+
NetworkVariable<T>.Read = NetworkVariable<T>.ReadNetworkSerializable;
20+
}
21+
}
22+
}

com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableHelper.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.netcode.gameobjects/Tests/Runtime/MultiInstanceHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ public static IEnumerator RunAndWaitForCondition(Action workload, Func<bool> pre
450450

451451
if (!waitResult.Result)
452452
{
453-
throw new Exception();
453+
Assert.Fail("Predicate condition failed");
454454
}
455455
}
456456

0 commit comments

Comments
 (0)