Skip to content

fix!: NativeList<T> causes crashes when used in RPCs [NCCBUG-106] #1901

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a397fe3
WIP: fixing crashes when passing a NativeList to an RPC
ShadauxCat Mar 24, 2022
87d681f
continued WIP, not ready for anything.
ShadauxCat Mar 29, 2022
11f292c
Finally got it working correctly, still need to write tests.
ShadauxCat Apr 8, 2022
8b10e96
Added tests for templated types, fixed some issues around the ILPP ge…
ShadauxCat Apr 25, 2022
fcf19d5
- Fixed failing tests (FixedString is not natively supported anymore …
ShadauxCat Apr 25, 2022
7dbb9bd
Merge branch 'develop' into fix/NativeList_crashes_when_used_in_RPCs
ShadauxCat Apr 25, 2022
b45a1b3
Removed some debug logging.
ShadauxCat Apr 25, 2022
ab80b0d
Standards
ShadauxCat Apr 25, 2022
ebdc659
Changelog
ShadauxCat Apr 25, 2022
6fcf43d
Fix testproject-tools-integration
ShadauxCat Apr 25, 2022
9593569
Merge branch 'develop' into fix/NativeList_crashes_when_used_in_RPCs
0xFA11 Apr 25, 2022
465303b
Slight refactor to let ILPP find more types it needs to generate init…
ShadauxCat Apr 26, 2022
6196a48
Merge remote-tracking branch 'origin/fix/NativeList_crashes_when_used…
ShadauxCat Apr 26, 2022
4dc77aa
Update com.unity.netcode.gameobjects/CHANGELOG.md
ShadauxCat Apr 26, 2022
8c17dc9
Merge branch 'develop' into fix/NativeList_crashes_when_used_in_RPCs
ShadauxCat Apr 26, 2022
a1df318
Standards.
ShadauxCat Apr 26, 2022
7df5876
Merge remote-tracking branch 'origin/fix/NativeList_crashes_when_used…
ShadauxCat Apr 26, 2022
3975d08
Merge branch 'develop' into fix/NativeList_crashes_when_used_in_RPCs
0xFA11 Apr 26, 2022
b0d8a8f
super minor touch
0xFA11 Apr 26, 2022
14f3259
xmldocs and comments
ShadauxCat Apr 27, 2022
8d84d08
Merge branch 'develop' into fix/NativeList_crashes_when_used_in_RPCs
ShadauxCat Apr 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Changed

- `unmanaged` structs are no longer universally accepted as RPC parameters because some structs (i.e., structs with pointers in them, such as `NativeList<T>`) can't be supported by the default memcpy struct serializer. Structs that are intended to be serialized across the network must add `INetworkSerializeByMemcpy` to the interface list (i.e., `struct Foo : INetworkSerializeByMemcpy`). This interface is empty and just serves to mark the struct as compatible with memcpy serialization. For external structs you can't edit, you can pass them to RPCs by wrapping them in `ForceNetworkSerializeByMemcpy<T>`. (#1901)

### Removed

### Fixed
Expand All @@ -19,6 +21,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
- Fixed client throwing an exception if it has messages in the outbound queue when processing the `NetworkEvent.Disconnect` event and is using UTP. (#1884)
- Fixed issue during client synchronization if 'ValidateSceneBeforeLoading' returned false it would halt the client synchronization process resulting in a client that was approved but not synchronized or fully connected with the server. (#1883)
- Fixed an issue where UNetTransport.StartServer would return success even if the underlying transport failed to start (#854)
- Passing generic types to RPCs no longer causes a native crash (#1901)

## [Unreleased]

Expand Down
30 changes: 30 additions & 0 deletions com.unity.netcode.gameobjects/Editor/CodeGen/CodeGenHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal static class CodeGenHelpers
public static readonly string ServerRpcSendParams_FullName = typeof(ServerRpcSendParams).FullName;
public static readonly string ServerRpcReceiveParams_FullName = typeof(ServerRpcReceiveParams).FullName;
public static readonly string INetworkSerializable_FullName = typeof(INetworkSerializable).FullName;
public static readonly string INetworkSerializeByMemcpy_FullName = typeof(INetworkSerializeByMemcpy).FullName;
public static readonly string UnityColor_FullName = typeof(Color).FullName;
public static readonly string UnityColor32_FullName = typeof(Color32).FullName;
public static readonly string UnityVector2_FullName = typeof(Vector2).FullName;
Expand Down Expand Up @@ -77,6 +78,35 @@ public static bool IsSubclassOf(this TypeDefinition typeDefinition, string class
return false;
}

public static string FullNameWithGenericParameters(this TypeReference typeReference, GenericParameter[] contextGenericParameters, TypeReference[] contextGenericParameterTypes)
{
var name = typeReference.FullName;
if (typeReference.HasGenericParameters)
{
name += "<";
for (var i = 0; i < typeReference.Resolve().GenericParameters.Count; ++i)
{
if (i != 0)
{
name += ", ";
}

for (var j = 0; j < contextGenericParameters.Length; ++j)
{
if (typeReference.GenericParameters[i].FullName == contextGenericParameters[i].FullName)
{
name += contextGenericParameterTypes[i].FullName;
break;
}
}
}

name += ">";
}

return name;
}

public static bool HasInterface(this TypeReference typeReference, string interfaceTypeFullName)
{
if (typeReference.IsArray)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,37 @@ public override bool WillProcess(ICompiledAssembly compiledAssembly) =>

private readonly List<DiagnosticMessage> m_Diagnostics = new List<DiagnosticMessage>();

private TypeReference ResolveGenericType(TypeReference type, List<TypeReference> typeStack)
{
var genericName = type.Name;
var lastType = (GenericInstanceType)typeStack[typeStack.Count - 1];
var resolvedType = lastType.Resolve();
typeStack.RemoveAt(typeStack.Count - 1);
for (var i = 0; i < resolvedType.GenericParameters.Count; ++i)
{
var parameter = resolvedType.GenericParameters[i];
if (parameter.Name == genericName)
{
var underlyingType = lastType.GenericArguments[i];
if (underlyingType.Resolve() == null)
{
return ResolveGenericType(underlyingType, typeStack);
}

return underlyingType;
}
}

return null;
}

public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
{
if (!WillProcess(compiledAssembly))
{
return null;
}


m_Diagnostics.Clear();

// read
Expand All @@ -50,16 +73,124 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
{
if (ImportReferences(mainModule))
{
var types = mainModule.GetTypes()
.Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializable_FullName) && !t.Resolve().IsAbstract && t.Resolve().IsValueType)
// Initialize all the delegates for various NetworkVariable types to ensure they can be serailized

// Find all types we know we're going to want to serialize.
// The list of these types includes:
// - Non-generic INetworkSerializable types
// - Non-Generic INetworkSerializeByMemcpy types
// - Enums that are not declared within generic types
// We can't process generic types because, to initialize a generic, we need a value
// for `T` to initialize it with.
var networkSerializableTypes = mainModule.GetTypes()
.Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializable_FullName) && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType)
.ToList();
var structTypes = mainModule.GetTypes()
.Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName) && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType)
.ToList();
var enumTypes = mainModule.GetTypes()
.Where(t => t.Resolve().IsEnum && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType)
.ToList();
// process `INetworkMessage` types
if (types.Count == 0)

// Now, to support generics, we have to do an extra pass.
// We look for any type that's a NetworkBehaviour type
// Then we look through all the fields in that type, finding any field whose type is
// descended from `NetworkVariableSerialization`. Then we check `NetworkVariableSerialization`'s
// `T` value, and if it's a generic, then we know it was missed in the above sweep and needs
// to be initialized. Now we have a full generic instance rather than a generic definition,
// so we can validly generate an initializer for that particular instance of the generic type.
var networkSerializableTypesSet = new HashSet<TypeReference>(networkSerializableTypes);
var structTypesSet = new HashSet<TypeReference>(structTypes);
var enumTypesSet = new HashSet<TypeReference>(enumTypes);
var typeStack = new List<TypeReference>();
foreach (var type in mainModule.GetTypes())
{
// Check if it's a NetworkBehaviour
if (type.IsSubclassOf(CodeGenHelpers.NetworkBehaviour_FullName))
{
// Iterate fields looking for NetworkVariableSerialization fields
foreach (var field in type.Fields)
{
// Get the field type and its base type
var fieldType = field.FieldType;
var baseType = fieldType.Resolve().BaseType;
// This type stack is used for resolving NetworkVariableSerialization's T value
// When looking at base types, we get the type definition rather than the
// type reference... which means that we get the generic definition with an
// undefined T rather than the instance with the type filled in.
// We then have to walk backward back down the type stack to resolve what T
// is.
typeStack.Clear();
typeStack.Add(fieldType);
// Iterate through the base types until we get to Object.
// Object is the base for everything so we'll stop when we hit that.
while (baseType.Name != mainModule.TypeSystem.Object.Name)
{
// If we've found a NetworkVariableSerialization type...
if (baseType.IsGenericInstance && baseType.Resolve() == m_NetworkVariableSerializationType)
{
// Then we need to figure out what T is
var genericType = (GenericInstanceType)baseType;
var underlyingType = genericType.GenericArguments[0];
if (underlyingType.Resolve() == null)
{
underlyingType = ResolveGenericType(underlyingType, typeStack);
}

// If T is generic...
if (underlyingType.IsGenericInstance)
{
// Then we pick the correct set to add it to and set it up
// for initialization.
if (underlyingType.HasInterface(CodeGenHelpers.INetworkSerializable_FullName))
{
networkSerializableTypesSet.Add(underlyingType);
}

if (underlyingType.HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName))
{
structTypesSet.Add(underlyingType);
}

if (underlyingType.Resolve().IsEnum)
{
enumTypesSet.Add(underlyingType);
}
}

break;
}

typeStack.Add(baseType);
baseType = baseType.Resolve().BaseType;
}
}
}
// We'll also avoid some confusion by ensuring users only choose one of the two
// serialization schemes - by method OR by memcpy, not both. We'll also do a cursory
// check that INetworkSerializeByMemcpy types are unmanaged.
else if (type.HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName))
{
if (type.HasInterface(CodeGenHelpers.INetworkSerializable_FullName))
{
m_Diagnostics.AddError($"{nameof(INetworkSerializeByMemcpy)} types may not implement {nameof(INetworkSerializable)} - choose one or the other.");
}
if (!type.IsValueType)
{
m_Diagnostics.AddError($"{nameof(INetworkSerializeByMemcpy)} types must be unmanaged types.");
}
}
}

if (networkSerializableTypes.Count + structTypes.Count + enumTypes.Count == 0)
{
return null;
}

CreateModuleInitializer(assemblyDefinition, types);
// Finally we add to the module initializer some code to initialize the delegates in
// NetworkVariableSerialization<T> for all necessary values of T, by calling initialization
// methods in NetworkVariableHelpers.
CreateModuleInitializer(assemblyDefinition, networkSerializableTypesSet.ToList(), structTypesSet.ToList(), enumTypesSet.ToList());
}
else
{
Expand Down Expand Up @@ -94,9 +225,15 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics);
}

private MethodReference m_InitializeDelegates_MethodRef;
private MethodReference m_InitializeDelegatesNetworkSerializable_MethodRef;
private MethodReference m_InitializeDelegatesStruct_MethodRef;
private MethodReference m_InitializeDelegatesEnum_MethodRef;

private const string k_InitializeMethodName = nameof(NetworkVariableHelper.InitializeDelegates);
private TypeDefinition m_NetworkVariableSerializationType;

private const string k_InitializeNetworkSerializableMethodName = nameof(NetworkVariableHelper.InitializeDelegatesNetworkSerializable);
private const string k_InitializeStructMethodName = nameof(NetworkVariableHelper.InitializeDelegatesStruct);
private const string k_InitializeEnumMethodName = nameof(NetworkVariableHelper.InitializeDelegatesEnum);

private bool ImportReferences(ModuleDefinition moduleDefinition)
{
Expand All @@ -106,11 +243,18 @@ private bool ImportReferences(ModuleDefinition moduleDefinition)
{
switch (methodInfo.Name)
{
case k_InitializeMethodName:
m_InitializeDelegates_MethodRef = moduleDefinition.ImportReference(methodInfo);
case k_InitializeNetworkSerializableMethodName:
m_InitializeDelegatesNetworkSerializable_MethodRef = moduleDefinition.ImportReference(methodInfo);
break;
case k_InitializeStructMethodName:
m_InitializeDelegatesStruct_MethodRef = moduleDefinition.ImportReference(methodInfo);
break;
case k_InitializeEnumMethodName:
m_InitializeDelegatesEnum_MethodRef = moduleDefinition.ImportReference(methodInfo);
break;
}
}
m_NetworkVariableSerializationType = moduleDefinition.ImportReference(typeof(NetworkVariableSerialization<>)).Resolve();
return true;
}

Expand Down Expand Up @@ -139,7 +283,7 @@ private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinit
// C# (that attribute doesn't exist in Unity, but the static module constructor still works)
// https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-5.0
// https://web.archive.org/web/20100212140402/http://blogs.msdn.com/junfeng/archive/2005/11/19/494914.aspx
private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeDefinition> networkSerializableTypes)
private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeReference> networkSerializableTypes, List<TypeReference> structTypes, List<TypeReference> enumTypes)
{
foreach (var typeDefinition in assembly.MainModule.Types)
{
Expand All @@ -151,9 +295,23 @@ private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeDefin

var instructions = new List<Instruction>();

foreach (var type in structTypes)
{
var method = new GenericInstanceMethod(m_InitializeDelegatesStruct_MethodRef);
method.GenericArguments.Add(type);
instructions.Add(processor.Create(OpCodes.Call, method));
}

foreach (var type in networkSerializableTypes)
{
var method = new GenericInstanceMethod(m_InitializeDelegates_MethodRef);
var method = new GenericInstanceMethod(m_InitializeDelegatesNetworkSerializable_MethodRef);
method.GenericArguments.Add(type);
instructions.Add(processor.Create(OpCodes.Call, method));
}

foreach (var type in enumTypes)
{
var method = new GenericInstanceMethod(m_InitializeDelegatesEnum_MethodRef);
method.GenericArguments.Add(type);
instructions.Add(processor.Create(OpCodes.Call, method));
}
Expand Down
Loading