Closed Pathoschild closed 7 years ago
The goal is to have a few simple utility methods relevant to most Stardew Valley mods, not to address every use case. (Mods that need more can use the .NET reflection API directly or use one of the existing reflection libraries.) Here are two possible approaches with that in mind.
One option is to create a few simple utility methods which let mods get a private value or method directly:
public interface IReflectionHelper
{
/// <summary>Get a private field value.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="parent">The parent object.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
TValue GetPrivateField<TValue>(object obj, string name, bool required = true);
/// <summary>Set a private field value.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="parent">The parent object.</param>
/// <param name="name">The field name.</param>
/// <param name="value">The value to set.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
void SetPrivateField<TValue>(object obj, string name, TValue value, bool required = true);
/// <summary>Get a private method.</summary>
/// <param name="parent">The parent object.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
MethodInfo GetPrivateMethod(object parent, string name, bool required = true);
}
This would be exposed as a mod property (e.g. this.ReflectionHelper
) or helper property (e.g. helper.Reflection
). Here's how you would...
string value = helper.Reflection.GetPrivateField<string>(obj, "fieldName");
helper.Reflection.SetPrivateField<string>(obj, "fieldName", value);
string value = (string)helper.Reflection.GetPrivateMethod(obj, "methodName").Invoke(arguments);
required: false
).MethodInfo
for methods).Another option is to provide a thin wrapper around .NET's build-in reflection API for validation and strong typing:
public interface IReflectionHelper
{
/// <summary>Get a private field value.</summary>
/// <typeparam name="TValue">The field type.</typeparam>
/// <param name="parent">The parent object which has the field.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
IFieldInfo<TValue> GetPrivateField<T>(object obj, string name, bool required = true);
/// <summary>Get a private method.</summary>
/// <param name="parent">The parent object which has the method.</param>
/// <param name="name">The field name.</param>
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
IMethodInfo GetPrivateMethod(object parent, string name, bool required = true);
}
The IFieldInfo<TValue>
would look something like this:
public interface IFieldInfo<TValue>
{
/// <summary>The reflection metadata.</summary>
FieldInfo FieldInfo { get; }
/// <summary>Get the field value.</summary>
TValue GetValue();
/// <summary>Set the field value.</summary>
//// <param name="value">The value to set.</param>
void SetValue(TValue value);
}
And the IMethodInfo
would look something like this:
public interface IMethodInfo
{
/// <summary>The reflection metadata.</summary>
MethodInfo MethodInfo { get; }
/// <summary>Invoke the method.</summary>
/// <typeparam name="TValue">The return type.</typeparam>
/// <param name="args">The method arguments.</param>
TValue Invoke<TValue>(params object[] arguments);
/// <summary>Invoke the method.</summary>
/// <param name="args">The method arguments.</param>
void Invoke(params object[] arguments);
}
This would be exposed as a mod property (e.g. this.ReflectionHelper
) or helper property (e.g. helper.Reflection
). Here's how you would...
string value = helper.Reflection.GetPrivateField<string>(obj, "fieldName").GetValue();
helper.Reflection.GetPrivateField<string>(obj, "fieldName").SetValue(value);
IFieldInfo<string> field = helper.Reflection.GetPrivateField<string>(obj, "fieldName");
string value = helper.Reflection.GetPrivateMethod(obj, "methodName").Invoke<string>(arguments);
IFieldInfo<TValue>
or underlying FieldValue
.Proposal B seems like a good approach simply due to the pro/con ratio largely out-weighing A. Having a standard utility for this will definitely be useful as I often have to drag the same reflection utilities between my own mods which can be a pain to maintain. 👍
I hereby suggest proposal C
where internally, it works like proposal B, but we wrap the mechanics so that users can use them as in proposal A, I have already setup a similar system myself, that also includes the technical side of making the reflection in both case A and B as efficient as doable: https://github.com/Entoarox/StardewMods/tree/master/Framework/Reflection
@Entoarox we can combine your approach nicely with the above proposals, so mod authors have cached access to both the full reflection API and simplified wrappers:
// use reflection API
FieldInfo field = GetPrivateField<string>(obj, "fieldName").FieldInfo;
// use wrapper
string value = GetPrivateField<string>(obj, "fieldName").GetValue();
// use it later
this.Field = GetPrivateField<string>(obj, "fieldName");
...
this.Field.GetValue();
This has a few advantages over any of the proposals individually:
GetPrivateField<T>(…)
will only show you field methods, so it's easy to write code quickly without scrolling through methods.field.HasValue()
by extending IFieldInfo<T>
. It would be much messier to add that to proposal A (e.g. you'd have FieldHasValue<T>(obj, "fieldName")
mixed in with the other methods, not even counting the static/non-static versions or flag).:+1: from me
Done in the develop
branch for the upcoming 1.4 release.
Implementation details:
The {typeName} object doesn't have a private '{fieldName}' instance field. Can't convert the private {typeName}.{fieldName} field from {fieldType} to {valueType}.
Here's what the reflection API looks like in practice:
See:
Closed as done; we can reopen it if anything comes up.
Mods often need to access private fields or methods, typically using fragile or unvalidated code. Provide a reflection helper that mods can optionally use instead.