Skip to content

Commit

Permalink
feat(clrcore-v2): Move to custom MsgPack serializer
Browse files Browse the repository at this point in the history
Allows for in-place conversion of serialized data.
* Replaces deserializer on events, exports, nui callbacks, and other refFuncs with our custom and more extensive msgpack (de)serializer.
* Has support for:
  - Class/struct field/property (de)serialization.
  - Class/struct constructor deserialization.
  - `Dictionary<K, V>`, `List<T>`, and `T[]`.
  - Primitives.
* Add msgpack-cs submodule (production branch)
* Temporary Player (de)serialization
* Command remapper no longer allows reordering of parameters, only remote/player id
  • Loading branch information
thorium-cfx committed May 17, 2024
1 parent e78ded5 commit 442697d
Show file tree
Hide file tree
Showing 23 changed files with 214 additions and 1,013 deletions.
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,7 @@
[submodule "vendor/boost-submodules/boost-tokenizer"]
path = vendor/boost-submodules/boost-tokenizer
url = https://github.com/boostorg/tokenizer
[submodule "vendor/msgpack-cs"]
path = vendor/msgpack-cs
url = https://github.com/citizenfx/msgpack-cs.git
branch = production
4 changes: 2 additions & 2 deletions code/client/clrcore-v2/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public class CommandAttribute : Attribute
public bool Restricted { get; set; }

/// <returns></returns>
/// <inheritdoc cref="Func.ConstructCommandRemapped(object, System.Reflection.MethodInfo)"/>
/// <inheritdoc cref="MsgPack.MsgPackDeserializer.ConstructCommandDelegateWithSource(object, System.Reflection.MethodInfo)"/>
public bool RemapParameters { get; set; } = false;
public CommandAttribute(string command, bool restricted = false)
{
Expand Down Expand Up @@ -77,7 +77,7 @@ public class KeyMapAttribute : Attribute
public string InputParameter { get; }

/// <returns></returns>
/// <inheritdoc cref="Func.ConstructCommandRemapped(object, System.Reflection.MethodInfo)"/>
/// <inheritdoc cref="MsgPack.MsgPackDeserializer.ConstructCommandDelegateWithSource(object, System.Reflection.MethodInfo)"/>
public bool RemapParameters { get; set; } = false;

/// <inheritdoc cref="KeyMapAttribute"/>
Expand Down
40 changes: 25 additions & 15 deletions code/client/clrcore-v2/BaseScript.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using CitizenFX.MsgPack;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Security;

Expand Down Expand Up @@ -33,9 +35,9 @@ protected event Func<Coroutine> Tick
remove => UnregisterTick(value);
}

private readonly List<KeyValuePair<int, DynFunc>> m_commands = new List<KeyValuePair<int, DynFunc>>();
private readonly List<KeyValuePair<int, MsgPackFunc>> m_commands = new List<KeyValuePair<int, MsgPackFunc>>();

private readonly Dictionary<string, DynFunc> m_nuiCallbacks = new Dictionary<string, DynFunc>();
private readonly Dictionary<string, MsgPackFunc> m_nuiCallbacks = new Dictionary<string, MsgPackFunc>();

#if REMOTE_FUNCTION_ENABLED
private readonly List<RemoteHandler> m_persistentFunctions = new List<RemoteHandler>();
Expand Down Expand Up @@ -92,22 +94,30 @@ internal void Initialize()
break;

case EventHandlerAttribute eventHandler:
RegisterEventHandler(eventHandler.Event, Func.Create(this, method), eventHandler.Binding);
RegisterEventHandler(eventHandler.Event, MsgPackDeserializer.CreateDelegate(this, method), eventHandler.Binding);
break;

case CommandAttribute command:
RegisterCommand(command.Command, Func.CreateCommand(this, method, command.RemapParameters), command.Restricted);
{
// Automatically remap methods with [Source] parameters
bool remap = command.RemapParameters || method.GetParameters().Any(p => Attribute.GetCustomAttribute(p, typeof(SourceAttribute)) != null);
RegisterCommand(command.Command, MsgPackDeserializer.CreateCommandDelegate(this, method, remap), command.Restricted);
}
break;
#if !IS_FXSERVER
case KeyMapAttribute keyMap:
RegisterKeyMap(keyMap.Command, keyMap.Description, keyMap.InputMapper, keyMap.InputParameter, Func.CreateCommand(this, method, keyMap.RemapParameters));
{
// Automatically remap methods with [Source] parameters
bool remap = keyMap.RemapParameters || method.GetParameters().Any(p => Attribute.GetCustomAttribute(p, typeof(SourceAttribute)) != null);
RegisterKeyMap(keyMap.Command, keyMap.Description, keyMap.InputMapper, keyMap.InputParameter, MsgPackDeserializer.CreateCommandDelegate(this, method, remap));
}
break;
case NuiCallbackAttribute nuiCallback:
RegisterNuiCallback(nuiCallback.CallbackName, Func.Create(this, method));
break;
#endif
case ExportAttribute export:
Exports.Add(export.Export, Func.Create(this, method), export.Binding);
Exports.Add(export.Export, MsgPackDeserializer.CreateDelegate(this, method), export.Binding);
break;
}
}
Expand Down Expand Up @@ -179,7 +189,7 @@ public void Disable()
// commands
for (int i = 0; i < m_commands.Count; ++i)
{
ReferenceFunctionManager.SetDelegate(m_commands[i].Key, (_0, _1) => null);
ReferenceFunctionManager.SetDelegate(m_commands[i].Key, delegate(Remote _0, ref MsgPackDeserializer _1) { return null; });
}

foreach (var nuiCallback in m_nuiCallbacks)
Expand Down Expand Up @@ -280,13 +290,13 @@ public void UnregisterTick(Func<Coroutine> tick)
#endregion

#region Events & Command registration
internal void RegisterEventHandler(string eventName, DynFunc deleg, Binding binding = Binding.Local) => EventHandlers[eventName].Add(deleg, binding);
internal void UnregisterEventHandler(string eventName, DynFunc deleg) => EventHandlers[eventName].Remove(deleg);
internal void RegisterEventHandler(string eventName, MsgPackFunc deleg, Binding binding = Binding.Local) => EventHandlers[eventName].Add(deleg, binding);
internal void UnregisterEventHandler(string eventName, MsgPackFunc deleg) => EventHandlers[eventName].Remove(deleg);

internal void RegisterCommand(string command, DynFunc dynFunc, bool isRestricted = true)
=> m_commands.Add(new KeyValuePair<int, DynFunc>(ReferenceFunctionManager.CreateCommand(command, dynFunc, isRestricted), dynFunc));
internal void RegisterCommand(string command, MsgPackFunc dynFunc, bool isRestricted = true)
=> m_commands.Add(new KeyValuePair<int, MsgPackFunc>(ReferenceFunctionManager.CreateCommand(command, dynFunc, isRestricted), dynFunc));

internal void RegisterKeyMap(string command, string description, string inputMapper, string inputParameter, DynFunc dynFunc)
internal void RegisterKeyMap(string command, string description, string inputMapper, string inputParameter, MsgPackFunc dynFunc)
{
#if !GTA_FIVE
throw new NotImplementedException();
Expand All @@ -295,15 +305,15 @@ internal void RegisterKeyMap(string command, string description, string inputMap
{
Native.CoreNatives.RegisterKeyMapping(command, description, inputMapper, inputParameter);
}
m_commands.Add(new KeyValuePair<int, DynFunc>(ReferenceFunctionManager.CreateCommand(command, dynFunc, false), dynFunc));
m_commands.Add(new KeyValuePair<int, MsgPackFunc>(ReferenceFunctionManager.CreateCommand(command, dynFunc, false), dynFunc));
#endif
}

#endregion

#region NUI Callback registration

internal void RegisterNuiCallback(string callbackName, DynFunc dynFunc)
internal void RegisterNuiCallback(string callbackName, MsgPackFunc dynFunc)
{
#if IS_FXSERVER
throw new NotImplementedException();
Expand All @@ -326,7 +336,7 @@ public void RegisterNuiCallback(string callbackName, Delegate delegateFn)
#if IS_FXSERVER
throw new NotImplementedException();
#endif
DynFunc dynFunc = Func.Create(delegateFn);
MsgPackFunc dynFunc = MsgPackDeserializer.CreateDelegate(delegateFn);
m_nuiCallbacks.Add(callbackName, dynFunc);
Native.CoreNatives.RegisterNuiCallback(callbackName, dynFunc);
}
Expand Down
153 changes: 0 additions & 153 deletions code/client/clrcore-v2/DynFunc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,6 @@ public static DynFunc Create(object target, MethodInfo method)
// no need to recreate it
public static DynFunc Create(DynFunc deleg) => deleg;

[SecurityCritical]
internal static DynFunc CreateCommand(object target, MethodInfo method, bool remap)
=> remap ? ConstructCommandRemapped(target, method) : Construct(target, method);

[SecurityCritical]
private static DynFunc Construct(object target, MethodInfo method)
{
Expand Down Expand Up @@ -216,140 +212,6 @@ private static DynFunc Construct(object target, MethodInfo method)
}
}

#if DYN_FUNC_CALLI
g.Emit(OpCodes.Ldc_I8, (long)method.MethodHandle.GetFunctionPointer());
g.EmitCalli(OpCodes.Calli, method.CallingConvention, method.ReturnType, parameterTypes, null);
#else
g.EmitCall(OpCodes.Call, method, null);
#endif

if (method.ReturnType == typeof(void))
g.Emit(OpCodes.Ldnull);
else
g.Emit(OpCodes.Box, method.ReturnType);

g.Emit(OpCodes.Ret);

Delegate dynFunc = lambda.CreateDelegate(typeof(DynFunc), target);

s_wrappedMethods.Add(method, dynFunc.Method);
s_dynfuncMethods.Add(dynFunc.Method, method);

return (DynFunc)dynFunc;
}

/// <summary>
/// If enabled creates a <see cref="DynFunc"/> that remaps input (<see cref="ushort"/> source, <see cref="object"/>[] arguments, <see cref="string"/> raw) to known types:<br />
/// <b>source</b>: <see cref="ushort"/>, <see cref="uint"/>, <see cref="int"/>, <see cref="bool"/>, <see cref="Remote"/>, or any type constructable from <see cref="Remote"/> including Player types.<br />
/// <b>arguments</b>: <see cref="object"/>[] or <see cref="string"/>[].<br />
/// <b>raw</b>: <see cref="string"/>
/// </summary>
/// <param name="target">Method's associated instance</param>
/// <param name="method">Method to wrap</param>
/// <returns>Dynamic invocable <see cref="DynFunc"/> with remapping and conversion support.</returns>
/// <exception cref="ArgumentException">When <see cref="SourceAttribute"/> is used on a non supported type.</exception>
/// <exception cref="TargetParameterCountException">When any requested parameter isn't supported.</exception>
[SecurityCritical]
private static DynFunc ConstructCommandRemapped(object target, MethodInfo method)
{
if (s_wrappedMethods.TryGetValue(method, out var existingMethod))
{
return (DynFunc)existingMethod.CreateDelegate(typeof(DynFunc), target);
}

ParameterInfo[] parameters = method.GetParameters();
#if DYN_FUNC_CALLI
Type[] parameterTypes = new Type[parameters.Length];
#endif
bool hasThis = (method.CallingConvention & CallingConventions.HasThis) != 0;

var lambda = new DynamicMethod($"{method.DeclaringType.FullName}.{method.Name}", typeof(object),
hasThis ? new[] { typeof(object), typeof(Remote), typeof(object[]) } : new[] { typeof(Remote), typeof(object[]) });

ILGenerator g = lambda.GetILGenerator();

OpCode ldarg_args;
if (hasThis)
{
g.Emit(OpCodes.Ldarg_0);
ldarg_args = OpCodes.Ldarg_2;
}
else
{
target = null;
ldarg_args = OpCodes.Ldarg_1;
}

for (int i = 0; i < parameters.Length; ++i)
{
var parameter = parameters[i];
var t = parameter.ParameterType;

#if DYN_FUNC_CALLI
parameterTypes[i] = t;
#endif
if (Attribute.IsDefined(parameter, typeof(SourceAttribute), true)) // source
{
g.Emit(ldarg_args);
g.Emit(OpCodes.Ldc_I4_0);
g.Emit(OpCodes.Ldelem_Ref);
g.Emit(OpCodes.Call, GetMethodInfo<object, ushort>(Convert.ToUInt16));

if (t.IsPrimitive)
{
if (t == typeof(int) || t == typeof(uint) || t == typeof(ushort))
{
// 16 bit integers are pushed onto the evaluation stack as 32 bit integers
continue;
}
else if (t == typeof(bool))
{
g.Emit(OpCodes.Ldc_I4_0);
g.Emit(OpCodes.Cgt_Un);
continue;
}
}
else if (t == typeof(Remote))
{
g.Emit(OpCodes.Call, ((Func<ushort, Remote>)Remote.Create).Method);
continue;
}
else
{
var constructor = t.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(Remote) }, null);
if (constructor != null)
{
g.Emit(OpCodes.Call, ((Func<ushort, Remote>)Remote.Create).Method);
g.Emit(OpCodes.Newobj, constructor);
continue;
}
}

throw new ArgumentException($"{nameof(SourceAttribute)} used on type {t}, this type can't be constructed with parameter Remote.");
}
else if (t == typeof(object[])) // arguments; simply pass it on
{
g.Emit(ldarg_args);
g.Emit(OpCodes.Ldc_I4_S, 1);
g.Emit(OpCodes.Ldelem_Ref);
}
else if (t == typeof(string[])) // arguments; convert to string[]
{
g.Emit(ldarg_args);
g.Emit(OpCodes.Ldc_I4_S, 1);
g.Emit(OpCodes.Ldelem_Ref);
g.EmitCall(OpCodes.Call, ((Func<object[], string[]>)ConvertToStringArray).Method, null);
}
else if (t == typeof(string)) // raw data; simply pass it on
{
g.Emit(ldarg_args);
g.Emit(OpCodes.Ldc_I4_S, 2);
g.Emit(OpCodes.Ldelem_Ref);
}
else
throw new TargetParameterCountException($"Command can't be registered with requested remapping, type {t} is not supported.");
}

#if DYN_FUNC_CALLI
g.Emit(OpCodes.Ldc_I8, (long)method.MethodHandle.GetFunctionPointer());
g.EmitCalli(OpCodes.Calli, method.CallingConvention, method.ReturnType, parameterTypes, null);
Expand Down Expand Up @@ -445,20 +307,5 @@ public static MethodInfo GetWrappedMethod(this DynFunc dynFunc)
{
return s_dynfuncMethods.TryGetValue(dynFunc.Method, out var existingMethod) ? existingMethod : dynFunc.Method;
}

#region Helper functions

internal static string[] ConvertToStringArray(object[] objects)
{
string[] result = new string[objects.Length];
for (int i = 0; i < objects.Length; ++i)
{
result[i] = objects[i]?.ToString();
}

return result;
}

#endregion
}
}
5 changes: 3 additions & 2 deletions code/client/clrcore-v2/Interop/Events.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CitizenFX.Core.Native;
using CitizenFX.MsgPack;
using System;
using System.ComponentModel;

Expand All @@ -17,7 +18,7 @@ public static class Events
/// <param name="eventName">name to listen for</param>
/// <param name="handler">delegate to call once triggered</param>
/// <param name="binding">limit calls to certain sources, e.g.: server only, client only</param>
public static void RegisterEventHandler(string eventName, DynFunc handler, Binding binding = Binding.Local)
public static void RegisterEventHandler(string eventName, MsgPackFunc handler, Binding binding = Binding.Local)
{
if (handler.Target is BaseScript script)
{
Expand All @@ -35,7 +36,7 @@ public static void RegisterEventHandler(string eventName, DynFunc handler, Bindi
/// </summary>
/// <param name="eventName">name to remove event for</param>
/// <param name="handler">delegate to remove</param>
public static void UnregisterEventHandler(string eventName, DynFunc handler)
public static void UnregisterEventHandler(string eventName, MsgPackFunc handler)
{
if (handler.Target is BaseScript script)
{
Expand Down
Loading

0 comments on commit 442697d

Please sign in to comment.