Skip to content

Commit 7b7ab49

Browse files
authored
fix: restore native fixed string support (#1961)
* WIP * Finished implementation, fixed tests, added some new tests. * Standards * changelog * Added some requested comments. * Added some comments.
1 parent c09a3de commit 7b7ab49

20 files changed

+674
-69
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
2020

2121
- Fixed issues when multiple `ConnectionApprovalCallback`s were registered (#1972)
2222
- Fixed endless dialog boxes when adding a NetworkBehaviour to a NetworkManager or vice-versa (#1947)
23+
- `FixedString` types can now be used in NetworkVariables and RPCs again without requiring a `ForceNetworkSerializeByMemcpy<>` wrapper (#1961)
2324

2425
## [1.0.0-pre.9] - 2022-05-10
2526

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Mono.Cecil;
77
using Mono.Cecil.Cil;
88
using Mono.Cecil.Rocks;
9+
using Unity.Collections;
910
using Unity.CompilationPipeline.Common.Diagnostics;
1011
using Unity.CompilationPipeline.Common.ILPostProcessing;
1112
using UnityEngine;
@@ -28,6 +29,7 @@ internal static class CodeGenHelpers
2829
public static readonly string ServerRpcReceiveParams_FullName = typeof(ServerRpcReceiveParams).FullName;
2930
public static readonly string INetworkSerializable_FullName = typeof(INetworkSerializable).FullName;
3031
public static readonly string INetworkSerializeByMemcpy_FullName = typeof(INetworkSerializeByMemcpy).FullName;
32+
public static readonly string IUTF8Bytes_FullName = typeof(IUTF8Bytes).FullName;
3133
public static readonly string UnityColor_FullName = typeof(Color).FullName;
3234
public static readonly string UnityColor32_FullName = typeof(Color32).FullName;
3335
public static readonly string UnityVector2_FullName = typeof(Vector2).FullName;

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

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Mono.Cecil;
77
using Mono.Cecil.Cil;
88
using Mono.Cecil.Rocks;
9+
using Unity.Collections;
910
using Unity.CompilationPipeline.Common.Diagnostics;
1011
using Unity.CompilationPipeline.Common.ILPostProcessing;
1112
using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor;
@@ -88,6 +89,15 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
8889
var structTypes = mainModule.GetTypes()
8990
.Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName) && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType)
9091
.ToList();
92+
// There are many FixedString types, but all of them share the interfaces INativeList<bool> and IUTF8Bytes.
93+
// INativeList<bool> provides the Length property
94+
// IUTF8Bytes provides GetUnsafePtr()
95+
// Those two are necessary to serialize FixedStrings efficiently
96+
// - otherwise we'd just be memcpying the whole thing even if
97+
// most of it isn't used.
98+
var fixedStringTypes = mainModule.GetTypes()
99+
.Where(t => t.Resolve().HasInterface(CodeGenHelpers.IUTF8Bytes_FullName) && t.HasInterface(m_INativeListBool_TypeRef.FullName) && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType)
100+
.ToList();
91101
var enumTypes = mainModule.GetTypes()
92102
.Where(t => t.Resolve().IsEnum && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType)
93103
.ToList();
@@ -102,6 +112,7 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
102112
var networkSerializableTypesSet = new HashSet<TypeReference>(networkSerializableTypes);
103113
var structTypesSet = new HashSet<TypeReference>(structTypes);
104114
var enumTypesSet = new HashSet<TypeReference>(enumTypes);
115+
var fixedStringTypesSet = new HashSet<TypeReference>(fixedStringTypes);
105116
var typeStack = new List<TypeReference>();
106117
foreach (var type in mainModule.GetTypes())
107118
{
@@ -141,26 +152,56 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
141152
underlyingType = ResolveGenericType(underlyingType, typeStack);
142153
}
143154

144-
// If T is generic...
145-
if (underlyingType.IsGenericInstance)
155+
// Then we pick the correct set to add it to and set it up
156+
// for initialization, if it's generic. We'll also use this moment to catch
157+
// any NetworkVariables with invalid T types at compile time.
158+
if (underlyingType.HasInterface(CodeGenHelpers.INetworkSerializable_FullName))
146159
{
147-
// Then we pick the correct set to add it to and set it up
148-
// for initialization.
149-
if (underlyingType.HasInterface(CodeGenHelpers.INetworkSerializable_FullName))
160+
if (underlyingType.IsGenericInstance)
150161
{
151162
networkSerializableTypesSet.Add(underlyingType);
152163
}
164+
}
153165

154-
if (underlyingType.HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName))
166+
else if (underlyingType.HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName))
167+
{
168+
if (underlyingType.IsGenericInstance)
155169
{
156170
structTypesSet.Add(underlyingType);
157171
}
172+
}
173+
else if (underlyingType.HasInterface(m_INativeListBool_TypeRef.FullName) && underlyingType.HasInterface(CodeGenHelpers.IUTF8Bytes_FullName))
174+
{
175+
if (underlyingType.IsGenericInstance)
176+
{
177+
fixedStringTypesSet.Add(underlyingType);
178+
}
179+
}
158180

159-
if (underlyingType.Resolve().IsEnum)
181+
else if (underlyingType.Resolve().IsEnum)
182+
{
183+
if (underlyingType.IsGenericInstance)
160184
{
161185
enumTypesSet.Add(underlyingType);
162186
}
163187
}
188+
else if (!underlyingType.Resolve().IsPrimitive)
189+
{
190+
bool methodExists = false;
191+
foreach (var method in m_FastBufferWriterType.Methods)
192+
{
193+
if (!method.HasGenericParameters && method.Parameters.Count == 1 && method.Parameters[0].ParameterType.Resolve() == underlyingType.Resolve())
194+
{
195+
methodExists = true;
196+
break;
197+
}
198+
}
199+
200+
if (!methodExists)
201+
{
202+
m_Diagnostics.AddError($"{type}.{field.Name}: {underlyingType} is not valid for use in {typeof(NetworkVariable<>).Name} types. Types must either implement {nameof(INetworkSerializeByMemcpy)} or {nameof(INetworkSerializable)}. If this type is external and you are sure its memory layout makes it serializable by memcpy, you can replace {underlyingType} with {typeof(ForceNetworkSerializeByMemcpy<>).Name}<{underlyingType}>, or you can create extension methods for {nameof(FastBufferReader)}.{nameof(FastBufferReader.ReadValueSafe)}(this {nameof(FastBufferReader)}, out {underlyingType}) and {nameof(FastBufferWriter)}.{nameof(FastBufferWriter.WriteValueSafe)}(this {nameof(FastBufferWriter)}, in {underlyingType}) to define serialization for this type.");
203+
}
204+
}
164205

165206
break;
166207
}
@@ -194,7 +235,7 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
194235
// Finally we add to the module initializer some code to initialize the delegates in
195236
// NetworkVariableSerialization<T> for all necessary values of T, by calling initialization
196237
// methods in NetworkVariableHelpers.
197-
CreateModuleInitializer(assemblyDefinition, networkSerializableTypesSet.ToList(), structTypesSet.ToList(), enumTypesSet.ToList());
238+
CreateModuleInitializer(assemblyDefinition, networkSerializableTypesSet.ToList(), structTypesSet.ToList(), enumTypesSet.ToList(), fixedStringTypesSet.ToList());
198239
}
199240
else
200241
{
@@ -232,12 +273,17 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
232273
private MethodReference m_InitializeDelegatesNetworkSerializable_MethodRef;
233274
private MethodReference m_InitializeDelegatesStruct_MethodRef;
234275
private MethodReference m_InitializeDelegatesEnum_MethodRef;
276+
private MethodReference m_InitializeDelegatesFixedString_MethodRef;
235277

236278
private TypeDefinition m_NetworkVariableSerializationType;
279+
private TypeDefinition m_FastBufferWriterType;
280+
281+
private TypeReference m_INativeListBool_TypeRef;
237282

238283
private const string k_InitializeNetworkSerializableMethodName = nameof(NetworkVariableHelper.InitializeDelegatesNetworkSerializable);
239284
private const string k_InitializeStructMethodName = nameof(NetworkVariableHelper.InitializeDelegatesStruct);
240285
private const string k_InitializeEnumMethodName = nameof(NetworkVariableHelper.InitializeDelegatesEnum);
286+
private const string k_InitializeFixedStringMethodName = nameof(NetworkVariableHelper.InitializeDelegatesFixedString);
241287

242288
private bool ImportReferences(ModuleDefinition moduleDefinition)
243289
{
@@ -256,9 +302,14 @@ private bool ImportReferences(ModuleDefinition moduleDefinition)
256302
case k_InitializeEnumMethodName:
257303
m_InitializeDelegatesEnum_MethodRef = moduleDefinition.ImportReference(methodInfo);
258304
break;
305+
case k_InitializeFixedStringMethodName:
306+
m_InitializeDelegatesFixedString_MethodRef = moduleDefinition.ImportReference(methodInfo);
307+
break;
259308
}
260309
}
261310
m_NetworkVariableSerializationType = moduleDefinition.ImportReference(typeof(NetworkVariableSerialization<>)).Resolve();
311+
m_NetworkVariableSerializationType = moduleDefinition.ImportReference(typeof(FastBufferWriter)).Resolve();
312+
m_INativeListBool_TypeRef = moduleDefinition.ImportReference(typeof(INativeList<bool>));
262313
return true;
263314
}
264315

@@ -287,7 +338,7 @@ private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinit
287338
// C# (that attribute doesn't exist in Unity, but the static module constructor still works)
288339
// https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-5.0
289340
// https://web.archive.org/web/20100212140402/http://blogs.msdn.com/junfeng/archive/2005/11/19/494914.aspx
290-
private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeReference> networkSerializableTypes, List<TypeReference> structTypes, List<TypeReference> enumTypes)
341+
private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeReference> networkSerializableTypes, List<TypeReference> structTypes, List<TypeReference> enumTypes, List<TypeReference> fixedStringTypes)
291342
{
292343
foreach (var typeDefinition in assembly.MainModule.Types)
293344
{
@@ -320,6 +371,13 @@ private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeRefer
320371
instructions.Add(processor.Create(OpCodes.Call, method));
321372
}
322373

374+
foreach (var type in fixedStringTypes)
375+
{
376+
var method = new GenericInstanceMethod(m_InitializeDelegatesFixedString_MethodRef);
377+
method.GenericArguments.Add(type);
378+
instructions.Add(processor.Create(OpCodes.Call, method));
379+
}
380+
323381
instructions.ForEach(instruction => processor.Body.Instructions.Insert(processor.Body.Instructions.Count - 1, instruction));
324382
break;
325383
}

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,14 @@ private MethodReference GetFastBufferWriterWriteMethod(string name, TypeReferenc
614614
#endif
615615

616616
var resolvedConstraintName = resolvedConstraint.FullNameWithGenericParameters(new[] { method.GenericParameters[0] }, new[] { checkType });
617+
if (constraint.IsGenericInstance)
618+
{
619+
var genericConstraint = (GenericInstanceType)constraint;
620+
if (genericConstraint.HasGenericArguments && genericConstraint.GenericArguments[0].Resolve() != null)
621+
{
622+
resolvedConstraintName = constraint.FullName;
623+
}
624+
}
617625
if ((resolvedConstraint.IsInterface && !checkType.HasInterface(resolvedConstraintName)) ||
618626
(resolvedConstraint.IsClass && !checkType.Resolve().IsSubclassOf(resolvedConstraintName)) ||
619627
(resolvedConstraint.Name == "ValueType" && !checkType.IsValueType))
@@ -749,6 +757,14 @@ private MethodReference GetFastBufferReaderReadMethod(string name, TypeReference
749757

750758

751759
var resolvedConstraintName = resolvedConstraint.FullNameWithGenericParameters(new[] { method.GenericParameters[0] }, new[] { checkType });
760+
if (constraint.IsGenericInstance)
761+
{
762+
var genericConstraint = (GenericInstanceType)constraint;
763+
if (genericConstraint.HasGenericArguments && genericConstraint.GenericArguments[0].Resolve() != null)
764+
{
765+
resolvedConstraintName = constraint.FullName;
766+
}
767+
}
752768

753769
if ((resolvedConstraint.IsInterface && !checkType.HasInterface(resolvedConstraintName)) ||
754770
(resolvedConstraint.IsClass && !checkType.Resolve().IsSubclassOf(resolvedConstraintName)) ||
@@ -1142,7 +1158,7 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA
11421158
}
11431159
else
11441160
{
1145-
m_Diagnostics.AddError(methodDefinition, $"Don't know how to serialize {paramType.Name} - implement {nameof(INetworkSerializable)}, tag memcpyable struct with {nameof(INetworkSerializeByMemcpy)}, or add an extension method for {nameof(FastBufferWriter)}.{k_WriteValueMethodName} to define serialization.");
1161+
m_Diagnostics.AddError(methodDefinition, $"{methodDefinition.Name} - Don't know how to serialize {paramType.Name}. RPC parameter types must either implement {nameof(INetworkSerializeByMemcpy)} or {nameof(INetworkSerializable)}. If this type is external and you are sure its memory layout makes it serializable by memcpy, you can replace {paramType} with {typeof(ForceNetworkSerializeByMemcpy<>).Name}<{paramType}>, or you can create extension methods for {nameof(FastBufferReader)}.{nameof(FastBufferReader.ReadValueSafe)}(this {nameof(FastBufferReader)}, out {paramType}) and {nameof(FastBufferWriter)}.{nameof(FastBufferWriter.WriteValueSafe)}(this {nameof(FastBufferWriter)}, in {paramType}) to define serialization for this type.");
11461162
continue;
11471163
}
11481164

@@ -1456,7 +1472,7 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition
14561472
}
14571473
else
14581474
{
1459-
m_Diagnostics.AddError(methodDefinition, $"Don't know how to serialize {paramType.Name} - implement {nameof(INetworkSerializable)}, tag memcpyable struct with {nameof(INetworkSerializeByMemcpy)}, or add an extension method for {nameof(FastBufferWriter)}.{k_WriteValueMethodName} to define serialization.");
1475+
m_Diagnostics.AddError(methodDefinition, $"{methodDefinition.Name} - Don't know how to serialize {paramType.Name}. RPC parameter types must either implement {nameof(INetworkSerializeByMemcpy)} or {nameof(INetworkSerializable)}. If this type is external and you are sure its memory layout makes it serializable by memcpy, you can replace {paramType} with {typeof(ForceNetworkSerializeByMemcpy<>).Name}<{paramType}>, or you can create extension methods for {nameof(FastBufferReader)}.{nameof(FastBufferReader.ReadValueSafe)}(this {nameof(FastBufferReader)}, out {paramType}) and {nameof(FastBufferWriter)}.{nameof(FastBufferWriter.WriteValueSafe)}(this {nameof(FastBufferWriter)}, in {paramType}) to define serialization for this type.");
14601476
continue;
14611477
}
14621478

com.unity.netcode.gameobjects/Editor/CodeGen/com.unity.netcode.editor.codegen.asmdef

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"name": "Unity.Netcode.Editor.CodeGen",
33
"rootNamespace": "Unity.Netcode.Editor.CodeGen",
44
"references": [
5-
"Unity.Netcode.Runtime"
5+
"Unity.Netcode.Runtime",
6+
"Unity.Collections"
67
],
78
"includePlatforms": [
89
"Editor"
@@ -26,4 +27,4 @@
2627
}
2728
],
2829
"noEngineReferences": false
29-
}
30+
}

0 commit comments

Comments
 (0)