Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parser attribute #97

Merged
merged 11 commits into from
Oct 27, 2024
Merged

Parser attribute #97

merged 11 commits into from
Oct 27, 2024

Conversation

Eivlisskiv
Copy link
Contributor

Usage Example:

[ConsoleParser(typeof(MyType), "My Type")]
public static bool TryParseMyType(string input, out object output)
{
  ...
}

@yasirkula
Copy link
Owner

Thank you for the PR! I find ConsoleParserAttribute useful (though I find ConsoleCustomTypeParserAttribute easier to understand) but I believe the current implementation is too complex for my taste. Here are my concerns:

  • I don't use LINQ for performance and GC allocation reasons (Unity's official optimization tips also discourage LINQ)
  • Right now, all types are stored in a TypeSearch[] array which IMO is unnecessary. For this case, I'd iterate over all types as before and store all ConsoleMethodAttributes occurrences in a List. And for all ConsoleParserAttribute occurrences, I'd evaluate them immediately. After iterating through all types, I'd iterate through the cached ConsoleMethodAttribute occurrences in the list. This implementation doesn't require TypeSearch and although it may have issues when another attribute is introduced, I'd like think of it when that happens. At the moment, there are no plans for additional attributes

If you don't feel like changing the current implementation, I could still implement this the way I want in a future update.

@Eivlisskiv
Copy link
Contributor Author

I'd be happy to make changes, I don't especially like the implementation either. I have a rough idea on how to support more attributes I'd like to try as well and see your thoughts on. I will start with making the changes you're requesting.

@Eivlisskiv
Copy link
Contributor Author

Eivlisskiv commented Oct 6, 2024

Added IConsoleMethodInfo to wrap all types of method info attributes.
TypeSearch is now a static class and simply returns a IEnumerable through GetConsoleMethods
The load order of each ConsoleMethodInfo is used to determine which is loaded first.
At order 0, it is immediately loaded and higher orders are loaded afterwards from 1 to x.

@yasirkula
Copy link
Owner

Thank you for the changes and sorry for my late responses!

  • The TryBuildFunction function can be removed entirely because Delegate.CreateDelegate already validates the method's signature (only out keyword isn't validated but I can live with that)
  • I don't use LINQ in any of my plugins and don't plan to change that
  • Structs are boxed when casted to an interface (IConsoleMethodInfo) so AFAIK there's no benefit in declaring the new types as structs. In fact, I'm completely against declaring those new types in the first place (as I've explained in my previous reply). While the proposed structure is more powerful, I believe it's less efficient due to additional allocations and the enumerator flow, and just unnecessarily complex for the current scenario. I want the implementation to be as simple and efficient as possible (I like KISS principle)

@Eivlisskiv
Copy link
Contributor Author

Eivlisskiv commented Oct 20, 2024

* The TryBuildFunction function can be removed entirely because Delegate.CreateDelegate already validates the method's signature (only `out` keyword isn't validated but I can live with that)

I personally prefer very clear error messages, but I suppose since the message contains the method definition example, it should remain clear anyways.

  • Renamed TryBuildFunction to TryBuildCustomParameterFunction.
  • Removed method info validation.
* I don't use LINQ in any of my plugins and don't plan to change that

There should not be any linq used, I've removed it all. I noticed I missed a using that I have now removed, but there's not linq methods being used.

  • Removed unused using System.Linq
* Structs are boxed when casted to an interface (IConsoleMethodInfo) so AFAIK there's no benefit in declaring the new types as structs. In fact, I'm completely against declaring those new types in the first place (as I've explained in my previous reply). While the proposed structure is more powerful, I believe it's less efficient due to additional allocations and the enumerator flow, and just unnecessarily complex for the current scenario. I want the implementation to be as simple and efficient as possible (I like KISS principle)

I wanted your feedback on the new enumerator first, but my intentions where to use the attributes to hold IConsoleMethodInfo instead. I believe doing so would align with your concerns, so I will go along with the changes I intended.

  • Removed IConsoleMethodInfo and the two structs.
  • Added ConsoleAttribute abstract class to replace IConsoleMethodInfo
  • Attributes extend ConsoleAttribute and define their order and load method.

I will be pushing the commits shortly. Some unrelated errors in my project have to be fixed before I can perform tests.

@yasirkula
Copy link
Owner

yasirkula commented Oct 26, 2024

I believe we can store all attributes in a List and then sort that list by Order. No need for TypeSearch and its Dictionary+enumerator approach anymore.

PS. A base class with Order property is good thinking 👍

Comment on lines 200 to 203
foreach( Type type in assembly.GetExportedTypes() )
IEnumerable<ConsoleAttribute> consoleMethods = TypeSearch.GetConsoleMethods(assembly.GetExportedTypes());
foreach (ConsoleAttribute consoleMethod in consoleMethods)
{
foreach( MethodInfo method in type.GetMethods( BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly ) )
{
foreach( object attribute in method.GetCustomAttributes( typeof( ConsoleMethodAttribute ), false ) )
{
ConsoleMethodAttribute consoleMethod = attribute as ConsoleMethodAttribute;
if( consoleMethod != null )
AddCommand( consoleMethod.Command, consoleMethod.Description, method, null, consoleMethod.ParameterNames );
}
}
consoleMethod.Load();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, can you restore this old code and instead of calling AddCommand, just fill the List? Then we can Sort that list and process its elements.

Comment on lines 372 to 394
public static void AddCustomParameterType(MethodInfo method, Type type, string readableName)
{
if (TryBuildCustomParameterFunction(method, out ParseFunction func, false))
AddCustomParameterType(type, func, readableName);
}

private static bool TryBuildCustomParameterFunction(MethodInfo method, out ParseFunction function, bool validate)
{
try
{
function = (ParseFunction)Delegate.CreateDelegate(typeof(ParseFunction), method);
return true;
}
catch (Exception e)
{
const string format = "Parser Method {0}.{1} is Invalid.\n{2}\nex: public static bool {1}(string input, out object result)";
string error = string.Format(format, method.DeclaringType.FullName, method.Name, e.Message);
Debug.LogError(error);
function = null;
return false;
}
}

Copy link
Owner

@yasirkula yasirkula Oct 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryBuildCustomParameterFunction isn't called elsewhere so could you combine these two functions?
PS. The function can be internal so that it can't be called from user code.
PPS. If we remove catch altogether, I believe we won't lose any important information since Delegate.CreateDelegate's exception message is informative by itself. In the worst case, if user can't understand the message, they can double click the exception and clearly see that a ParseFunction signature is expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to break when an exception is throw or just to skip this one method? If we don't catch here, all the next methods will be ignored.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with an unhandled exception in this case. There is an error that must be resolved and in both approaches, that error will be logged as soon as the developer enters Play mode. An exception will force the developer to resolve the issue which is a win-win in my book: it's simpler and more effective.

@@ -439,7 +455,7 @@ private static void AddCommand( string command, string description, string metho
AddCommand( command, description, method, instance, parameterNames );
}

private static void AddCommand( string command, string description, MethodInfo method, object instance, string[] parameterNames )
public static void AddCommand( string command, string description, MethodInfo method, object instance, string[] parameterNames )
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this method and the other mentioned before, making them internal so they can't be called from user code means they won't be able to use these methods if the users make some additional custom Console Attributes.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That shouldn't be a problem since there are already Console Attributes for these functions. If a user creates an alternative to ConsoleMethodAttribute, then I'd very much like to hear their reasoning.

When a user types DebugLogConsole.AddCommand to add a command via Scripting API, I want them to use the documented public functions and not these particular functions.

public readonly Type type;
public readonly string readableName;

public override int Order => 0;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I love this syntax but it isn't supported on very old Unity versions and I plan to support them for as long as possible. Could you use old syntax here and at other similar location(s): public override int Order { get { return 0; } }

Comment on lines 200 to 213
foreach( Type type in assembly.GetExportedTypes() )
Type[] types = assembly.GetExportedTypes();
List<ConsoleAttribute> methods = new();
const BindingFlags BindingAttr = BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly;
for (int t = 0; t < types.Length; t++)
{
foreach( MethodInfo method in type.GetMethods( BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly ) )
MethodInfo[] typeMethods = types[t].GetMethods(BindingAttr);
for (int m = 0; m < typeMethods.Length; m++)
{
foreach( object attribute in method.GetCustomAttributes( typeof( ConsoleMethodAttribute ), false ) )
MethodInfo method = typeMethods[m];
IEnumerable<ConsoleAttribute> attributes = method.GetCustomAttributes<ConsoleAttribute>(true);
foreach (ConsoleAttribute attribute in attributes)
{
ConsoleMethodAttribute consoleMethod = attribute as ConsoleMethodAttribute;
if( consoleMethod != null )
AddCommand( consoleMethod.Command, consoleMethod.Description, method, null, consoleMethod.ParameterNames );
attribute.SetMethod(method);
methods.Add(attribute);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the changes.

  • Could we make the diff minimalistic by changing only the necessary lines? This will help with Git's Blame/Annotate functionality. Ideally, only the code inside the innermost foreach should change.
  • I'd advise against casting arrays or lists to IEnumerable because foreach(list) and foreach(array) (these use duck typing) are more optimized than foreach(ienumerable) (you can check out their GC allocations in Profiler).
  • new() isn't supported on older Unity versions. I also prefer this approach in my personal projects but atm not in my plugins.

Copy link
Contributor Author

@Eivlisskiv Eivlisskiv Oct 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* Could we make the diff minimalistic by changing only the necessary lines? This will help with Git's Blame/Annotate functionality. Ideally, only the code inside the innermost foreach should change.

Sure, I personally try to always for loops over foreach loops.

* I'd advise against casting arrays or lists to IEnumerable because `foreach(list)` and `foreach(array)` (these use duck typing) are more optimized than `foreach(ienumerable)` (you can check out their GC allocations in Profiler).

GetCustomAttributes returns a IEnumerable and not a list or array. I'll switch to the non generic one like the original code did.

* new() isn't supported on older Unity versions. I also prefer this approach in my personal projects but atm not in my plugins.

Force of habit, will fix!

@yasirkula
Copy link
Owner

Thank you for the changes.

@yasirkula yasirkula merged commit fe646cf into yasirkula:master Oct 27, 2024
yasirkula added a commit that referenced this pull request Oct 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants