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

.NET Core hosting: Call managed method without knowing the types at compile time #40203

Closed
fsinisi90 opened this issue Jul 31, 2020 · 14 comments
Closed
Labels
area-Host question Answer questions and provide assistance, not an issue with source code or documentation.
Milestone

Comments

@fsinisi90
Copy link

With load_assembly_and_get_function_pointer you can call a managed method by using the default signature or by using a custom signature (see this sample). But in both cases, you have to know the types at compile time.

Is there a way to call a managed method but knowing the types at runtime?

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Jul 31, 2020
@Dotnet-GitSync-Bot
Copy link
Collaborator

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@fsinisi90
Copy link
Author

@mangod9 mangod9 added area-Meta question Answer questions and provide assistance, not an issue with source code or documentation. labels Jul 31, 2020
@ghost
Copy link

ghost commented Aug 3, 2020

Tagging subscribers to this area: @vitek-karas, @swaroop-sridhar, @agocke
See info in area-owners.md if you want to be subscribed.

@jeffschwMSFT
Copy link
Member

@fsinisi90 can you elaborate on what you had in mind? I have seen people use a known .NET entry point, and that entry point uses reflection to then construct the call to a more dynamic call site. Would that work in your case?

@jeffschwMSFT jeffschwMSFT removed the untriaged New issue has not been triaged by the area owner label Aug 3, 2020
@jeffschwMSFT jeffschwMSFT added this to the Future milestone Aug 3, 2020
@fsinisi90
Copy link
Author

@jeffschwMSFT Thanks for answering.

Basically, I have a C++ "engine" that allows C# scripting. I'm loading the managed assembly and I need to execute methods that I'm reading from an XML file, so I don't know the signatures at compile time (it's, of course, restricted to some primitive types like int, float, and string). With Mono, I was able to provide a string containing the signature, then create an array of void* pointers and call the method with the desired arguments. Like this:

std::string TypeMethodDescStr = "Program:MyMethodFromXML(int,single,string)";
TypeMethodDesc = mono_method_desc_new(TypeMethodDescStr.c_str(), NULL);

poMethod = mono_method_desc_search_in_image(TypeMethodDesc, m_poImage);

void* poArgs[poCommand->oArguments().size()];

if (poCommand->oArguments().size() > 0)
{
    QVector<QStringRef> oTypes = poCommand->strSignature().splitRef(',');

    // Prepare arguments
    for (int i = 0; i < oTypes.size(); i++)
    {
        if (oTypes.at(i) == "string")
        {
            QString strArg = poCommand->oArguments()[i];
            poArgs[i] = mono_string_new(mono_domain_get(), strArg.toUtf8());
        }
        else if (oTypes.at(i) == "single") // float in C#
        {
            float fArg = poCommand->oArguments()[i].toFloat();
            cout << fArg << endl;
            poArgs[i] = &fArg;
        }
        else
        {
            int iArg = poCommand->oArguments()[i].toInt();
            cout << iArg << endl;
            poArgs[i] = &iArg;
        }
    }
}

cout << "C++: Running the static method: " << TypeMethodDescStr.c_str() << endl;
mono_runtime_invoke(poMethod, nullptr, (void**)poArgs, nullptr);

I was looking for a similar functionality using .NET Core but I guess it's not present yet. In that case, I'll have to do exactly what you proposed: have a known entry point and then use reflection to make the call.

My problem now is how to marshal the data... If you have any code snippets or suggestions it will be highly appreciated.

@jeffschwMSFT
Copy link
Member

Here is a quick sample that has an arbitrary reflection based invoke.

using System;
using System.Reflection;

public class MyType
{
	public string CallMe(int i, float f)
	{
		return $"{i} and {f}";
	}
}

class Program
{
	public static string CallArbitraryMethod(string typeName, string methodName, string[] paramNames, object[] arguments)
	{
		// create the type
		var type = Type.GetType(typeName);
		if (type == null) return null;

		// create an instance using the default constructor
		var ctor = type.GetConstructor(new Type[0]);
		if (ctor == null) return null;
		var obj = ctor.Invoke(null);
		if (obj == null) return null;

		// construct an array of parameter types
		var paramTypes = new Type[ paramNames.Length ];
		for(int i=0; i<paramNames.Length; i++)
		{
			switch(paramNames[i].ToUpper())
			{
				case "INT": paramTypes[i] = typeof(int); break;
				case "FLOAT": paramTypes[i] = typeof(float); break;
				// etc.
				default: return null;
			}
		}

		// get the target method
		var method = type.GetMethod(methodName, paramTypes);
		if (method == null) return null;

		// invoke and return
		return (string)method.Invoke(obj, arguments);
	}

	static void Main(string[] args)
	{
		var result = CallArbitraryMethod("MyType", "CallMe", new string[] {"int", "float"}, new object[] {5, 10.5f});
		Console.WriteLine($"{result}");
	}
}

My problem now is how to marshal the data... If you have any code snippets or suggestions it will be highly appreciated.

What types are you looking to marshal? strings, arrays of strings, IntPtr's, and int's should all be very standard.

@fsinisi90
Copy link
Author

fsinisi90 commented Feb 10, 2021

@jeffschwMSFT Thank you very much, it works having a known entry point and then making the call using reflection. Is there a way to call non-static C# methods from C++? Or are you planning to add that feature?

@jeffschwMSFT
Copy link
Member

Not really, the interface between managed and native is c-style methods. If you have control of both sides it certainly is possible given you have access to cache objects on the managed code and can dispatch calls. I am including area owners who may have more to share.

cc @agocke @AaronRobinsonMSFT

@AaronRobinsonMSFT
Copy link
Member

Or are you planning to add that feature?

@fsinisi90 Not at the runtime level. This is one of those things that people can already do without any runtime feature. The likely future of this scenario is leveraging a Roslyn source generator or other source generation tool and autogenerating the boiler plate code for a natural C++ experience. The DNNE tool is part of the way there. The two biggest missing pieces are handling all the marshalling and making the generated C exports align with a C++ object model.

@fsinisi90
Copy link
Author

Not really, the interface between managed and native is c-style methods. If you have control of both sides it certainly is possible given you have access to cache objects on the managed code and can dispatch calls. I am including area owners who may have more to share.

cc @agocke @AaronRobinsonMSFT

Ok, I see. Thank you both for the quick answer! Yes, I have control over both sides. Could you share an example of how to make the mentioned call?

@AaronRobinsonMSFT
Copy link
Member

@fsinisi90 Many people have asked for an example - thanks for the push. I have updated DNNE with an example on one approach - https://github.com/AaronRobinsonMSFT/DNNE/blob/master/test/ExportingAssembly/InstanceExports.cs.

@fsinisi90
Copy link
Author

@AaronRobinsonMSFT Thank you. I noticed that .NET 5.0 is a minimum requirement. Is there a way to accomplish the same with .NET Core? My scenario is a C++ application hosting .NET Core. I'd like to instantiate managed objects and call methods from C/C++ directly. I can take care of creating the glue code and the marshalling. I was already able to call methods as callbacks using delegates. Any advice would be appreciated.

Here's what I did so far.

C#:

public class Example
{
	public int Add(int iFirst, int iSecond)
	{
		return iFirst + iSecond;
	}
}

public class Program
{
	[SuppressUnmanagedCodeSecurity]
	[DllImport("__Internal", CallingConvention = global::System.Runtime.InteropServices.CallingConvention.Cdecl, EntryPoint = "ExecuteDelegate")]
	internal static extern int __ExecuteDelegate(int iFirst, int iSecond, System.Delegate oHandler);
	
	public delegate int Delegate(int iFirst, int iSecond);
	
	public static void Initialize()
	{
		Example oExample = new Example();
		Delegate oHandler = oExample.Add;
		int iResult = __ExecuteDelegate(20, 30, oHandler);
		Console.WriteLine(iResult);
	}
}

C++:

extern "C"
{

__declspec(dllexport) int ExecuteDelegate(int iFirst, int iSecond, int (*f)(int, int))
{
    return f(iFirst, iSecond);
}

}

@AaronRobinsonMSFT
Copy link
Member

I noticed that .NET 5.0 is a minimum requirement. Is there a way to accomplish the same with .NET Core?

Yep. The .NET 5.0 requirement is simply because the optimized path is using C# function pointers and the UnmanagedCallersOnly API - both exclusive to .NET 5+. I think the DNNE tooling would work using the fallback mechanism instead - https://github.com/AaronRobinsonMSFT/DNNE#experimental-attribute. However, if DNNE isn't desired then your existing approach can be manually done using the function exports here but creating Delegates instead - the approach illustrated in your post.

@fsinisi90
Copy link
Author

Very clear, thanks again. I hope this is going to be useful to others as well - I'm closing the issue now.

@dotnet dotnet locked as resolved and limited conversation to collaborators Mar 17, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-Host question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
None yet
Development

No branches or pull requests

5 participants