Skip to content

Commit 1db5de5

Browse files
committed
WIP hook emitter to generate static hooks
Works for some namespaces, WIP on others
1 parent bc941d5 commit 1db5de5

1 file changed

Lines changed: 346 additions & 0 deletions

File tree

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
/*
2+
Copyright (C) 2024 DeathCradle
3+
4+
This file is part of Open Terraria API v3 (OTAPI)
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
using Mono.Cecil;
20+
using Mono.Cecil.Cil;
21+
using System;
22+
using System.Linq;
23+
24+
namespace ModFramework;
25+
26+
[MonoMod.MonoModIgnore]
27+
public static class HookEmitter
28+
{
29+
/// <summary>
30+
/// Creates a new type in the assembly to hoist the hook events.
31+
/// </summary>
32+
/// <param name="type"></param>
33+
/// <returns></returns>
34+
static TypeDefinition GetOrCreateHookType(TypeDefinition type)
35+
{
36+
var hookTypeName = "HookEvents." + type.FullName;
37+
var hookType = type.Module.Types.SingleOrDefault(x => x.FullName == hookTypeName);
38+
if (hookType is null)
39+
{
40+
hookType = new(
41+
"HookEvents." + type.Namespace,
42+
type.Name,
43+
TypeAttributes.Class | TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.Public | TypeAttributes.BeforeFieldInit
44+
);
45+
hookType.BaseType = type.Module.TypeSystem.Object;
46+
type.Module.Types.Add(hookType);
47+
}
48+
return hookType;
49+
}
50+
51+
static string GetUniqueName(MethodDefinition method)
52+
{
53+
// generate a name, with consideration to overloads
54+
var name = method.Name;
55+
if (method.DeclaringType.Methods.Count(x => x.Name == name) > 1 && method.Parameters.Count > 0)
56+
{
57+
// overloads are detected, append parameter types to the name
58+
name += "_" + string.Join("_", method.Parameters.Select(y => y.ParameterType.Name));
59+
}
60+
return name;
61+
}
62+
63+
const String HookReturnValueName = "HookReturnValue";
64+
const String ContinueExecutionName = "ContinueExecution";
65+
66+
static TypeDefinition CreateHookEventArgs(TypeDefinition hookType, MethodDefinition hookDefinition, string? name = null)
67+
{
68+
var hookEventName = name ?? (hookDefinition.Name + "EventArgs");
69+
TypeDefinition hookEvent = new(
70+
"", //hookType.Namespace,
71+
hookEventName,
72+
TypeAttributes.Class | TypeAttributes.BeforeFieldInit | TypeAttributes.NestedPublic | TypeAttributes.Sealed,
73+
hookType.Module.ImportReference(typeof(EventArgs))
74+
);
75+
hookType.NestedTypes.Add(hookEvent);
76+
77+
var resultType = hookDefinition.Module.TypeSystem.Boolean;
78+
FieldDefinition resultField = new(ContinueExecutionName, FieldAttributes.Public, resultType);
79+
80+
// if the method has a return type, add a field for it
81+
var hasReturnValue = hookDefinition.ReturnType != hookDefinition.Module.TypeSystem.Void;
82+
if (hasReturnValue)
83+
{
84+
FieldDefinition returnField = new(HookReturnValueName, FieldAttributes.Public, hookDefinition.ReturnType);
85+
hookEvent.Fields.Add(returnField);
86+
}
87+
88+
// for each parameter in the method, create a field
89+
foreach (var param in hookDefinition.Parameters)
90+
{
91+
var paramType = param.ParameterType;
92+
FieldDefinition paramField = new(param.Name, FieldAttributes.Public, paramType);
93+
hookEvent.Fields.Add(paramField);
94+
}
95+
96+
// create ctor, calling base ctor
97+
MethodDefinition ctor = new (".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, hookDefinition.Module.TypeSystem.Void);
98+
ctor.Body = new MethodBody(ctor);
99+
var il = ctor.Body.GetILProcessor();
100+
il.Emit(OpCodes.Ldarg_0);
101+
il.Emit(OpCodes.Call, hookDefinition.Module.ImportReference(typeof(object).GetConstructors().Single()));
102+
// Set ContinueExecution to true
103+
il.Emit(OpCodes.Ldarg_0);
104+
il.Emit(OpCodes.Ldc_I4_1);
105+
il.Emit(OpCodes.Stfld, resultField);
106+
il.Emit(OpCodes.Ret);
107+
hookEvent.Methods.Add(ctor);
108+
109+
hookEvent.Fields.Add(resultField);
110+
return hookEvent;
111+
}
112+
113+
static MethodDefinition CreateInvokeMethod(TypeDefinition hookType, FieldDefinition eventField, TypeDefinition hookEventArgsType, string? name = null)
114+
{
115+
var methodName = name ?? $"Invoke{eventField.Name.TrimStart('_')}";
116+
117+
// Define the `Invoke` method
118+
MethodDefinition invokeMethod = new(
119+
methodName,
120+
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static,
121+
hookEventArgsType
122+
);
123+
124+
// Add parameters: object sender, hookEventArgsType args
125+
var senderFieldName = "instanceAsSender";
126+
while (hookType.Fields.Any(x => x.Name == senderFieldName))
127+
senderFieldName = "_" + senderFieldName;
128+
ParameterDefinition senderParam = new(senderFieldName, ParameterAttributes.None, hookType.Module.TypeSystem.Object);
129+
invokeMethod.Parameters.Add(senderParam);
130+
131+
//var returnValueField = hookEventArgsType.Fields.SingleOrDefault(x => x.Name == HookReturnValueName);
132+
//if (returnValueField is not null)
133+
//{
134+
// // add a param type "byref" for the return value
135+
// ParameterDefinition returnValueParam = new(HookReturnValueName, ParameterAttributes.Out, returnValueField.FieldType);
136+
// invokeMethod.Parameters.Add(returnValueParam);
137+
//}
138+
139+
// instead of an event args, intake the parameters so each call doesnt need to new up itself.
140+
foreach (var field in hookEventArgsType.Fields.Where(x => x.Name != ContinueExecutionName && x.Name != HookReturnValueName))
141+
{
142+
ParameterDefinition prm = new(field.Name, ParameterAttributes.None, field.FieldType);
143+
invokeMethod.Parameters.Add(prm);
144+
}
145+
146+
// Create a GenericInstanceType for EventHandler<HookEventArgsType>
147+
var eventHandlerGenericType = hookType.Module.ImportReference(typeof(EventHandler<>));
148+
GenericInstanceType genericEventHandlerType = new(eventHandlerGenericType)
149+
{
150+
GenericArguments = { hookEventArgsType }
151+
};
152+
153+
// Import the "Invoke" method of EventHandler<HookEventArgsType>
154+
var eventHandlerInvokeMethod = eventHandlerGenericType.Resolve().Methods.First(m => m.Name == "Invoke");
155+
MethodReference invokeMethodReference = new(
156+
eventHandlerInvokeMethod.Name,
157+
hookType.Module.TypeSystem.Void,
158+
genericEventHandlerType
159+
)
160+
{
161+
HasThis = true
162+
};
163+
164+
// Add parameters to the invokeMethodReference
165+
invokeMethodReference.Parameters.Add(new (hookType.Module.TypeSystem.Object)); // sender
166+
invokeMethodReference.Parameters.Add(new (eventHandlerInvokeMethod.Parameters[1].ParameterType)); // args - see EventHandler<>.Invoke, il is !0
167+
168+
// Generate IL for the Invoke method
169+
var il = invokeMethod.Body.GetILProcessor();
170+
var returnLabel = il.Create(OpCodes.Ldloc_0);
171+
172+
// Create the event args instance from the method parameters
173+
VariableDefinition vrb = new (hookEventArgsType);
174+
invokeMethod.Body.Variables.Add(vrb);
175+
il.Emit(OpCodes.Newobj, hookEventArgsType.Methods.Single(x => x.Name == ".ctor")); // Create a new instance of the event args
176+
// Set the fields of the event args instance
177+
foreach (var prm in invokeMethod.Parameters.Skip(1 /*sender*/))
178+
{
179+
il.Emit(OpCodes.Dup); // Load the event args instance
180+
il.Emit(OpCodes.Ldarg, prm); // Load the parameter
181+
il.Emit(OpCodes.Stfld, hookEventArgsType.Fields.Single(x => x.Name == prm.Name)); // Set the field
182+
}
183+
il.Emit(OpCodes.Stloc_0); // Store the event args instance
184+
185+
// Check if the event is not null
186+
il.Emit(OpCodes.Ldsfld, eventField); // Load the static event field
187+
il.Emit(OpCodes.Brfalse_S, returnLabel); // If null, skip invocation
188+
189+
// Invoke the event delegate
190+
il.Emit(OpCodes.Ldsfld, eventField); // Load the static event field
191+
il.Emit(OpCodes.Ldarg_0); // Load the sender (first parameter)
192+
//il.Emit(OpCodes.Ldarg_1); // Load the args (second parameter)
193+
il.Emit(OpCodes.Ldloc_0); // Load the event args instance
194+
il.Emit(OpCodes.Callvirt, invokeMethodReference); // Call the Invoke method on the delegate
195+
196+
// Return args.Result
197+
il.Append(returnLabel);
198+
//il.Emit(OpCodes.Ldfld, hookEventArgsType.Fields.Single(x => x.Name == "ContinueExecution")); // Load the Result field
199+
// Return the event args variable
200+
il.Emit(OpCodes.Ldloc_0);
201+
il.Emit(OpCodes.Ret);
202+
203+
// Add the Invoke method to the type
204+
hookType.Methods.Add(invokeMethod);
205+
206+
return invokeMethod;
207+
}
208+
209+
/// <summary>
210+
/// Creates a replacement method for the original method.
211+
/// </summary>
212+
/// <param name="original"></param>
213+
/// <param name="eventInvoke"></param>
214+
/// <returns></returns>
215+
static MethodDefinition CreateReplacement(MethodDefinition original, MethodDefinition eventInvoke)
216+
{
217+
var eventArgs = eventInvoke.ReturnType.Resolve();
218+
var hookReturnValueField = eventArgs.Fields.SingleOrDefault(x => x.Name == HookReturnValueName);
219+
220+
MethodDefinition methodDefinition = new(
221+
original.Name,
222+
original.Attributes,
223+
original.ReturnType
224+
);
225+
226+
foreach (var param in original.Parameters)
227+
methodDefinition.Parameters.Add(new ParameterDefinition(param.Name, param.Attributes, param.ParameterType));
228+
229+
var il = methodDefinition.Body.GetILProcessor();
230+
231+
VariableDefinition eventArgsVariable = new(eventInvoke.ReturnType);
232+
methodDefinition.Body.Variables.Add(eventArgsVariable);
233+
234+
il.Emit(original.IsStatic ? OpCodes.Ldnull : OpCodes.Ldarg_0);
235+
for (int i = 0; i < methodDefinition.Parameters.Count; i++)
236+
il.Emit(OpCodes.Ldarg, methodDefinition.Parameters[i]);
237+
il.Emit(OpCodes.Call, eventInvoke);
238+
239+
// store the event args in a local variable
240+
il.Emit(OpCodes.Stloc, eventArgsVariable);
241+
242+
// use ContinueExecutionName to determine whether to continue or not
243+
il.Emit(OpCodes.Ldloc, eventArgsVariable);
244+
il.Emit(OpCodes.Ldfld, eventInvoke.ReturnType.Resolve().Fields.Single(x => x.Name == ContinueExecutionName));
245+
246+
// the event invoke is a boolen, if false, return else invoke the original method
247+
var returnLabel = hookReturnValueField is not null ? il.Create(OpCodes.Ldloc, eventArgsVariable) : il.Create(OpCodes.Ret);
248+
249+
il.Emit(OpCodes.Brfalse_S, returnLabel);
250+
251+
il.Emit(OpCodes.Ldarg_0);
252+
// for each event arg field, load it onto the stack to the original method
253+
foreach (var field in eventArgs.Fields.Where(x => x.Name != ContinueExecutionName && x.Name != HookReturnValueName))
254+
{
255+
il.Emit(OpCodes.Ldloc, eventArgsVariable);
256+
il.Emit(OpCodes.Ldfld, field);
257+
}
258+
il.Emit(OpCodes.Call, original.Module.ImportReference(original));
259+
il.Emit(OpCodes.Ret);
260+
261+
il.Append(returnLabel);
262+
263+
if (hookReturnValueField is not null)
264+
{
265+
il.Emit(OpCodes.Ldfld, hookReturnValueField);
266+
il.Emit(OpCodes.Ret);
267+
}
268+
269+
return methodDefinition;
270+
}
271+
272+
/// <summary>
273+
/// Creates a hook for a single method.
274+
/// </summary>
275+
/// <param name="definition"></param>
276+
/// <param name="modder"></param>
277+
public static void CreateHook(this MethodDefinition definition, ModFwModder modder)
278+
{
279+
// create hook type
280+
// create event in hook type
281+
// rename current method
282+
// put one in it's place
283+
// call an event
284+
// check whether to continue or not, using a simple bool flag
285+
286+
var uniqueName = GetUniqueName(definition);
287+
var hookType = GetOrCreateHookType(definition.DeclaringType);
288+
289+
var hookEventArgs = CreateHookEventArgs(hookType, definition, name: $"{uniqueName}EventArgs");
290+
var (hookField, _) = definition.CreateEvent(hookType, hookEventArgs, name: uniqueName);
291+
var newMethod = CreateInvokeMethod(hookType, hookField, hookEventArgs, name: $"Invoke{uniqueName}");
292+
293+
var replacement = CreateReplacement(definition, newMethod);
294+
definition.DeclaringType.Methods.Add(replacement);
295+
296+
// rename the original method
297+
definition.Name = $"hooked_{definition.Name}";
298+
299+
// remove any overrides etc
300+
definition.Attributes &= ~MethodAttributes.Virtual;
301+
definition.Attributes &= ~MethodAttributes.NewSlot;
302+
}
303+
304+
/// <summary>
305+
/// Creates hooks for an entire type.
306+
/// </summary>
307+
/// <param name="definition">The assembly definition</param>
308+
/// <param name="modder">Modder instance</param>
309+
/// <param name="methodNames">Optional list of methods to process, if empty all methods will be processed</param>
310+
public static void CreateHooks(this TypeDefinition definition, ModFwModder modder, params string[] methodNames)
311+
{
312+
foreach (var method in definition.Methods.Where(x => x.HasBody &&
313+
(methodNames.Length == 0 || methodNames.Contains(x.Name)) &&
314+
// not a constructor
315+
!x.IsConstructor &&
316+
// not an event
317+
!(definition.Events.Any(evt => evt.AddMethod == x || evt.RemoveMethod == x))
318+
).ToList())
319+
method.CreateHook(modder);
320+
}
321+
322+
/// <summary>
323+
/// Creates hooks for an entire module.
324+
/// </summary>
325+
/// <param name="definition">The module definition</param>
326+
/// <param name="modder">Modder instance</param>
327+
/// <param name="typeNames">Optional list of types to process, if empty all types will be processed</param>
328+
public static void CreateHooks(this ModuleDefinition definition, ModFwModder modder, params string[] typeNames)
329+
{
330+
foreach (var type in definition.Types.ToList())
331+
if (typeNames.Length == 0 || typeNames.Contains(type.FullName))
332+
type.CreateHooks(modder);
333+
}
334+
335+
/// <summary>
336+
/// Creates hooks for an entire assembly.
337+
/// </summary>
338+
/// <param name="definition">The assembly definition</param>
339+
/// <param name="modder">Modder instance</param>
340+
/// <param name="typeNames">Optional list of types to process, if empty all types will be processed</param>
341+
public static void CreateHooks(this AssemblyDefinition definition, ModFwModder modder, params string[] typeNames)
342+
{
343+
foreach (var module in definition.Modules.ToList())
344+
module.CreateHooks(modder, typeNames);
345+
}
346+
}

0 commit comments

Comments
 (0)