Pathoschild / SMAPI

The modding API for Stardew Valley.
https://smapi.io/
GNU Lesser General Public License v3.0
1.71k stars 258 forks source link

Provide reflection helper for mods #185

Closed Pathoschild closed 7 years ago

Pathoschild commented 7 years ago

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.

Pathoschild commented 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.

Proposal A: simple utility methods

Interface

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);
}

Usage

This would be exposed as a mod property (e.g. this.ReflectionHelper) or helper property (e.g. helper.Reflection). Here's how you would...

Pros & cons

Proposal B: wrapped reflection API

Interface

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);
}

Usage

This would be exposed as a mod property (e.g. this.ReflectionHelper) or helper property (e.g. helper.Reflection). Here's how you would...

Pros & cons

tstaples commented 7 years ago

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. 👍

Entoarox commented 7 years ago

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

Pathoschild commented 7 years ago

@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:

Entoarox commented 7 years ago

:+1: from me

Pathoschild commented 7 years ago

Done in the develop branch for the upcoming 1.4 release.

Implementation details:

Here's what the reflection API looks like in practice:

screenshot of intellisense

See:

Pathoschild commented 7 years ago

Closed as done; we can reopen it if anything comes up.