feat: new generic Read and Write methods for all types (#2301)

Currently in mirror,  there is no way for Mirror itself to invoke
readers and writers for types it does not know about.

This causes us to have to generate code for lists, dictionaries and messages.

This PR makes it so that if there is a reader and writer anywhere in the
code for type X, then the writer can be invoked like this:

```cs
X x = ...;
writer.Write<X>(x);
```

and the reader can be invoked like this:
```cs
X x = reader.Read<X>();
```
This commit is contained in:
Paul Pacheco 2020-10-01 17:13:30 -05:00 committed by GitHub
parent 16c919eae9
commit 85252c3d84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 234 additions and 13 deletions

View File

@ -139,6 +139,22 @@ public static MethodReference MakeHostInstanceGeneric(this MethodReference self,
return Weaver.CurrentAssembly.MainModule.ImportReference(reference);
}
/// <summary>
/// Given a field of a generic class such as Writer<T>.write,
/// and a generic instance such as ArraySegment`int
/// Creates a reference to the specialized method ArraySegment`int`.get_Count
/// <para> Note that calling ArraySegment`T.get_Count directly gives an invalid IL error </para>
/// </summary>
/// <param name="self"></param>
/// <param name="instanceType">Generic Instance eg Writer<int></param>
/// <returns></returns>
public static FieldReference SpecializeField(this FieldReference self, GenericInstanceType instanceType)
{
FieldReference reference = new FieldReference(self.Name, self.FieldType, instanceType);
return Weaver.CurrentAssembly.MainModule.ImportReference(reference);
}
public static CustomAttribute GetCustomAttribute<TAttribute>(this ICustomAttributeProvider method)
{
foreach (CustomAttribute ca in method.CustomAttributes)

View File

@ -1,7 +1,11 @@
// finds all readers and writers and register them
using System.IO;
using System;
using System.Linq;
using Mono.CecilX;
using Mono.CecilX.Cil;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
namespace Mirror.Weaver
{
@ -14,20 +18,12 @@ public static void Process(AssemblyDefinition CurrentAssembly)
foreach (Assembly unityAsm in CompilationPipeline.GetAssemblies())
{
if (unityAsm.name != CurrentAssembly.Name.Name)
if (unityAsm.name == "Mirror")
{
try
using (DefaultAssemblyResolver asmResolver = new DefaultAssemblyResolver())
using (AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(unityAsm.outputPath, new ReaderParameters { ReadWrite = false, ReadSymbols = false, AssemblyResolver = asmResolver }))
{
using (DefaultAssemblyResolver asmResolver = new DefaultAssemblyResolver())
using (AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(unityAsm.outputPath, new ReaderParameters { ReadWrite = false, ReadSymbols = false, AssemblyResolver = asmResolver }))
{
ProcessAssemblyClasses(CurrentAssembly, assembly);
}
}
catch (FileNotFoundException)
{
// During first import, this gets called before some assemblies
// are built, just skip them
ProcessAssemblyClasses(CurrentAssembly, assembly);
}
}
}
@ -97,5 +93,54 @@ static void LoadDeclaredReaders(AssemblyDefinition currentAssembly, TypeDefiniti
Readers.Register(method.ReturnType, currentAssembly.MainModule.ImportReference(method));
}
}
private static bool IsEditorAssembly(AssemblyDefinition currentAssembly)
{
return currentAssembly.MainModule.AssemblyReferences.Any(assemblyReference =>
assemblyReference.Name == nameof(UnityEditor)
) ;
}
/// <summary>
/// Creates a method that will store all the readers and writers into
/// <see cref="Writer{T}.write"/> and <see cref="Reader{T}.read"/>
///
/// The method will be marked InitializeOnLoadMethodAttribute so it gets
/// executed before mirror runtime code
/// </summary>
/// <param name="currentAssembly"></param>
public static void InitializeReaderAndWriters(AssemblyDefinition currentAssembly)
{
var rwInitializer = new MethodDefinition("InitReadWriters", MethodAttributes.Public |
MethodAttributes.Static,
WeaverTypes.Import(typeof(void)));
System.Reflection.ConstructorInfo attributeconstructor = typeof(RuntimeInitializeOnLoadMethodAttribute).GetConstructor(new [] { typeof(RuntimeInitializeLoadType)});
CustomAttribute customAttributeRef = new CustomAttribute(currentAssembly.MainModule.ImportReference(attributeconstructor));
customAttributeRef.ConstructorArguments.Add(new CustomAttributeArgument(WeaverTypes.Import<RuntimeInitializeLoadType>(), RuntimeInitializeLoadType.BeforeSceneLoad));
rwInitializer.CustomAttributes.Add(customAttributeRef);
if (IsEditorAssembly(currentAssembly))
{
// editor assembly, add InitializeOnLoadMethod too. Useful for the editor tests
System.Reflection.ConstructorInfo initializeOnLoadConstructor = typeof(InitializeOnLoadMethodAttribute).GetConstructor(new Type[0]);
CustomAttribute initializeCustomConstructorRef = new CustomAttribute(currentAssembly.MainModule.ImportReference(initializeOnLoadConstructor));
rwInitializer.CustomAttributes.Add(initializeCustomConstructorRef);
}
ILProcessor worker = rwInitializer.Body.GetILProcessor();
Writers.InitializeWriters(worker);
Readers.InitializeReaders(worker);
worker.Append(worker.Create(OpCodes.Ret));
Weaver.WeaveLists.ConfirmGeneratedCodeClass();
TypeDefinition generateClass = Weaver.WeaveLists.generateContainerClass;
generateClass.Methods.Add(rwInitializer);
}
}
}

View File

@ -411,5 +411,40 @@ static void ReadAllFields(TypeReference variable, ILProcessor worker)
fields++;
}
}
/// <summary>
/// Save a delegate for each one of the readers into <see cref="Reader{T}.read"/>
/// </summary>
/// <param name="worker"></param>
internal static void InitializeReaders(ILProcessor worker)
{
ModuleDefinition module = Weaver.CurrentAssembly.MainModule;
TypeReference genericReaderClassRef = module.ImportReference(typeof(Reader<>));
System.Reflection.FieldInfo fieldInfo = typeof(Reader<>).GetField(nameof(Reader<object>.read));
FieldReference fieldRef = module.ImportReference(fieldInfo);
TypeReference networkReaderRef = module.ImportReference(typeof(NetworkReader));
TypeReference funcRef = module.ImportReference(typeof(Func<,>));
MethodReference funcConstructorRef = module.ImportReference(typeof(Func<,>).GetConstructors()[0]);
foreach (MethodReference readFunc in readFuncs.Values)
{
TypeReference dataType = readFunc.ReturnType;
// create a Func<NetworkReader, T> delegate
worker.Append(worker.Create(OpCodes.Ldnull));
worker.Append(worker.Create(OpCodes.Ldftn, readFunc));
GenericInstanceType funcGenericInstance = funcRef.MakeGenericInstanceType(networkReaderRef, dataType);
MethodReference funcConstructorInstance = funcConstructorRef.MakeHostInstanceGeneric(funcGenericInstance);
worker.Append(worker.Create(OpCodes.Newobj, funcConstructorInstance));
// save it in Writer<T>.write
GenericInstanceType genericInstance = genericReaderClassRef.MakeGenericInstanceType(dataType);
FieldReference specializedField = fieldRef.SpecializeField(genericInstance);
worker.Append(worker.Create(OpCodes.Stsfld, specializedField));
}
}
}
}

View File

@ -300,6 +300,8 @@ static bool Weave(string assName, IEnumerable<string> dependencies)
if (modified)
{
ReaderWriterProcessor.InitializeReaderAndWriters(CurrentAssembly);
// write to outputDir if specified, otherwise perform in-place write
WriterParameters writeParams = new WriterParameters { WriteSymbols = true };
CurrentAssembly.Write(writeParams);

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using Mono.CecilX;
using Mono.CecilX.Cil;
using Mono.CecilX.Rocks;
namespace Mirror.Weaver
{
@ -433,5 +434,41 @@ private static void GenerateFor(ILProcessor worker, Action body)
worker.Append(worker.Create(OpCodes.Ldloc_0));
worker.Append(worker.Create(OpCodes.Blt, labelBody));
}
/// <summary>
/// Save a delegate for each one of the writers into <see cref="Writer{T}.write"/>
/// </summary>
/// <param name="worker"></param>
internal static void InitializeWriters(ILProcessor worker)
{
ModuleDefinition module = Weaver.CurrentAssembly.MainModule;
TypeReference genericWriterClassRef = module.ImportReference(typeof(Writer<>));
System.Reflection.FieldInfo fieldInfo = typeof(Writer<>).GetField(nameof(Writer<object>.write));
FieldReference fieldRef = module.ImportReference(fieldInfo);
TypeReference networkWriterRef = module.ImportReference(typeof(NetworkWriter));
TypeReference actionRef = module.ImportReference(typeof(Action<,>));
MethodReference actionConstructorRef = module.ImportReference(typeof(Action<,>).GetConstructors()[0]);
foreach (MethodReference writerMethod in writeFuncs.Values)
{
TypeReference dataType = writerMethod.Parameters[1].ParameterType;
// create a Action<NetworkWriter, T> delegate
worker.Append(worker.Create(OpCodes.Ldnull));
worker.Append(worker.Create(OpCodes.Ldftn, writerMethod));
GenericInstanceType actionGenericInstance = actionRef.MakeGenericInstanceType(networkWriterRef, dataType);
MethodReference actionRefInstance = actionConstructorRef.MakeHostInstanceGeneric(actionGenericInstance);
worker.Append(worker.Create(OpCodes.Newobj, actionRefInstance));
// save it in Writer<T>.write
GenericInstanceType genericInstance = genericWriterClassRef.MakeGenericInstanceType(dataType);
FieldReference specializedField = fieldRef.SpecializeField(genericInstance);
worker.Append(worker.Create(OpCodes.Stsfld, specializedField));
}
}
}
}

View File

@ -13,6 +13,18 @@
namespace Mirror
{
/// <summary>
/// a class that holds readers for the different types
/// Note that c# creates a different static variable for each
/// type
/// This will be populated by the weaver
/// </summary>
/// <typeparam name="T"></typeparam>
public static class Reader<T>
{
public static Func<NetworkReader, T> read;
}
// Note: This class is intended to be extremely pedantic, and
// throw exceptions whenever stuff is going slightly wrong.
// The exceptions will be handled in NetworkServer/NetworkClient.
@ -108,6 +120,16 @@ public override string ToString()
{
return "NetworkReader pos=" + Position + " len=" + Length + " buffer=" + BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count);
}
/// <summary>
/// Reads any data type that mirror supports
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Read<T>()
{
return Reader<T>.read(this);
}
}
// Mirror's Weaver automatically detects all NetworkReader function types,

View File

@ -5,6 +5,18 @@
namespace Mirror
{
/// <summary>
/// a class that holds writers for the different types
/// Note that c# creates a different static variable for each
/// type
/// This will be populated by the weaver
/// </summary>
/// <typeparam name="T"></typeparam>
public static class Writer<T>
{
public static Action<NetworkWriter, T> write;
}
/// <summary>
/// Binary stream Writer. Supports simple types, buffers, arrays, structs, and nested types
/// <para>Use <see cref="NetworkWriterPool.GetWriter">NetworkWriter.GetWriter</see> to reduce memory allocation</para>
@ -154,6 +166,16 @@ public void WriteUInt64(ulong value)
}
public void WriteInt64(long value) => WriteUInt64((ulong)value);
/// <summary>
/// Writes any type that mirror supports
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
public void Write<T>(T value)
{
Writer<T>.write(this, value);
}
}

View File

@ -0,0 +1,31 @@
using NUnit.Framework;
namespace Mirror.Tests
{
[TestFixture]
public class NetworkReaderWriterTest
{
[Test]
public void TestIntWriterNotNull()
{
Assert.That(Writer<int>.write, Is.Not.Null);
}
[Test]
public void TestIntReaderNotNull()
{
Assert.That(Reader<int>.read, Is.Not.Null);
}
[Test]
public void TestAccessingCustomWriterAndReader()
{
NetworkWriter writer = new NetworkWriter();
writer.Write<int>(3);
NetworkReader reader = new NetworkReader(writer.ToArray());
int copy = reader.Read<int>();
Assert.That(copy, Is.EqualTo(3));
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4db12e3e883ac4c3aac177c638ee93e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: