问题 如何使用代码生成动态创建C#方法?


为了在C中定义可由Lua调用的方法,它必须匹配给定的签名并使用Lua API来检索参数并返回结果。我正在编写一个Lua的C#包装器,我对能够调用任意C#方法而不使它们遵循这些约定感兴趣。当包装在像D这样的东西时,可以使用模板系统为任何给定的方法动态创建这个粘合代码。我认为在C#中也可以这样做,但是使用动态代码生成。

C API看起来像这样,并且生成的代码将通过我的库的较低级别部分来操纵它,其中P / Invokes Lua C库。

static int foo (lua_State *L)
{
    int n = lua_gettop(L);    /* number of arguments */
    lua_Number sum = 0;
    int i;
    for (i = 1; i <= n; i++)
    {
        if (!lua_isnumber(L, i)) 
        {
            lua_pushstring(L, "incorrect argument");
            lua_error(L);
        }
        sum += lua_tonumber(L, i);
    }
    lua_pushnumber(L, sum/n);        /* first result */
    lua_pushnumber(L, sum);         /* second result */
    return 2;                   /* number of results */
}

所以基本上我的想法是采用C#方法,反映其参数和返回值,生成(或从缓存中检索)一个方法,该方法使用上面的Lua API传递这些参数并返回这些返回类型,最后将该方法推送到Lua 。因此,当从Lua调用C#函数时,它看起来像lua - > magic wrapper function - >普通的C#函数。

谢谢。


7138
2018-03-01 08:31


起源



答案:


如果我理解你想要的东西,你似乎有两个选择:

  1. 使用CodeDOM 在运行时生成并动态编译代码。
  2. 发出实际的C#源代码,并在运行时将其动态编译为可调用的程序集。

CodeDom是一种毛茸茸的,非常低级的代码。我们的想法是有一个C#语言的对象模型。首先实例化Code​​TypeDeclaration - 这将生成一个类型或类。然后你添加属性和字段 - 在这里你可能会添加 DllImport p / invoke函数的声明。然后对类型使用不同的CodeDOM添加方法 - 这将是您插入生成的方法的位置。无论你喜欢什么,你都可以公开,静态。

CodeDOM看起来像这样:

System.Type mt= a[0].GetType();

System.CodeDom.CodeTypeDeclaration class1 = new System.CodeDom.CodeTypeDeclaration(mt.Name);
class1.IsClass=true;
class1.TypeAttributes = System.Reflection.TypeAttributes.Public;
class1.Comments.Add(new System.CodeDom.CodeCommentStatement("Wrapper class for " + mt.Name));

System.CodeDom.CodeConstructor ctor;
ctor= new System.CodeDom.CodeConstructor();
ctor.Attributes = System.CodeDom.MemberAttributes.Public;
ctor.Comments.Add(new System.CodeDom.CodeCommentStatement("the null constructor"));
class1.Members.Add(ctor);
ctor.Statements.Add(new System.CodeDom.CodeAssignStatement(new System.CodeDom.CodeVariableReferenceExpression("m_wrapped"), new System.CodeDom.CodeObjectCreateExpression(mt)));

ctor= new System.CodeDom.CodeConstructor();
ctor.Attributes = System.CodeDom.MemberAttributes.Public;
ctor.Comments.Add(new System.CodeDom.CodeCommentStatement("the 'copy' constructor"));
class1.Members.Add(ctor);
ctor.Parameters.Add(new System.CodeDom.CodeParameterDeclarationExpression(mt,"X"));
ctor.Statements.Add(new System.CodeDom.CodeAssignStatement(new System.CodeDom.CodeVariableReferenceExpression("m_wrapped"), new System.CodeDom.CodeVariableReferenceExpression("X")));

// embed a local (private) copy of the wrapped type
System.CodeDom.CodeMemberField field1;
field1= new System.CodeDom.CodeMemberField();
field1.Attributes = System.CodeDom.MemberAttributes.Private;
field1.Name= "m_wrapped";
field1.Type=new System.CodeDom.CodeTypeReference(mt);
class1.Members.Add(field1);

...

它继续下去。并且。如你所见,它变得非常难看。然后你编译它,我没有显示。我假设你不想采取这种方法。


我发现CodeDom使用非常苛刻;相反,现在当我需要动态生成的程序集时,我会 发出实际的C#代码通常通过模板,在内存中的字符串,并编译 。这对我的目的来说简单得多。编译看起来像这样:

var cp = new System.CodeDom.Compiler.CompilerParameters {
  ReferencedAssemblies.Add(filesystemLocation), // like /R: option on csc.exe
  GenerateInMemory = true,    // you will get a System.Reflection.Assembly back
  GenerateExecutable = false, // Dll
  IncludeDebugInformation = false,
  CompilerOptions = ""
};

var csharp = new Microsoft.CSharp.CSharpCodeProvider();

// this actually runs csc.exe:
System.CodeDom.Compiler.CompilerResults cr = 
      csharp.CompileAssemblyFromSource(cp, LiteralSource);


// cr.Output contains the output from the command

if (cr.Errors.Count != 0)
{
    // handle errors
}

System.Reflection.Assembly a = cr.CompiledAssembly;

// party on the type here, either via reflection...
System.Type t = a.GetType("TheDynamicallyGeneratedType");

// or via a wellknown interface

在上面的代码中, LiteralSource 包含要编译的源代码。正如我所说,我通过阅读模板并填写空白来生成这个。


14
2018-03-04 03:05



这是一个有趣的方法,我从未想过编写实际的C#然后编译它。可能我最终会做什么。谢谢。 - jsimmons
你如何在磁盘上生成类? - jcolebrand
@jcolebrand - 我不清楚你在问什么,但我认为你应该发一个新问题。 - Cheeso
是的,但这显然是一个棘手的谈话。 chat.stackoverflow.com/transcript/message/2889886#2889886 我已经尝试过如何处理它,如果你想知道我有什么,也许可以参与谈话 - jcolebrand


答案:


如果我理解你想要的东西,你似乎有两个选择:

  1. 使用CodeDOM 在运行时生成并动态编译代码。
  2. 发出实际的C#源代码,并在运行时将其动态编译为可调用的程序集。

CodeDom是一种毛茸茸的,非常低级的代码。我们的想法是有一个C#语言的对象模型。首先实例化Code​​TypeDeclaration - 这将生成一个类型或类。然后你添加属性和字段 - 在这里你可能会添加 DllImport p / invoke函数的声明。然后对类型使用不同的CodeDOM添加方法 - 这将是您插入生成的方法的位置。无论你喜欢什么,你都可以公开,静态。

CodeDOM看起来像这样:

System.Type mt= a[0].GetType();

System.CodeDom.CodeTypeDeclaration class1 = new System.CodeDom.CodeTypeDeclaration(mt.Name);
class1.IsClass=true;
class1.TypeAttributes = System.Reflection.TypeAttributes.Public;
class1.Comments.Add(new System.CodeDom.CodeCommentStatement("Wrapper class for " + mt.Name));

System.CodeDom.CodeConstructor ctor;
ctor= new System.CodeDom.CodeConstructor();
ctor.Attributes = System.CodeDom.MemberAttributes.Public;
ctor.Comments.Add(new System.CodeDom.CodeCommentStatement("the null constructor"));
class1.Members.Add(ctor);
ctor.Statements.Add(new System.CodeDom.CodeAssignStatement(new System.CodeDom.CodeVariableReferenceExpression("m_wrapped"), new System.CodeDom.CodeObjectCreateExpression(mt)));

ctor= new System.CodeDom.CodeConstructor();
ctor.Attributes = System.CodeDom.MemberAttributes.Public;
ctor.Comments.Add(new System.CodeDom.CodeCommentStatement("the 'copy' constructor"));
class1.Members.Add(ctor);
ctor.Parameters.Add(new System.CodeDom.CodeParameterDeclarationExpression(mt,"X"));
ctor.Statements.Add(new System.CodeDom.CodeAssignStatement(new System.CodeDom.CodeVariableReferenceExpression("m_wrapped"), new System.CodeDom.CodeVariableReferenceExpression("X")));

// embed a local (private) copy of the wrapped type
System.CodeDom.CodeMemberField field1;
field1= new System.CodeDom.CodeMemberField();
field1.Attributes = System.CodeDom.MemberAttributes.Private;
field1.Name= "m_wrapped";
field1.Type=new System.CodeDom.CodeTypeReference(mt);
class1.Members.Add(field1);

...

它继续下去。并且。如你所见,它变得非常难看。然后你编译它,我没有显示。我假设你不想采取这种方法。


我发现CodeDom使用非常苛刻;相反,现在当我需要动态生成的程序集时,我会 发出实际的C#代码通常通过模板,在内存中的字符串,并编译 。这对我的目的来说简单得多。编译看起来像这样:

var cp = new System.CodeDom.Compiler.CompilerParameters {
  ReferencedAssemblies.Add(filesystemLocation), // like /R: option on csc.exe
  GenerateInMemory = true,    // you will get a System.Reflection.Assembly back
  GenerateExecutable = false, // Dll
  IncludeDebugInformation = false,
  CompilerOptions = ""
};

var csharp = new Microsoft.CSharp.CSharpCodeProvider();

// this actually runs csc.exe:
System.CodeDom.Compiler.CompilerResults cr = 
      csharp.CompileAssemblyFromSource(cp, LiteralSource);


// cr.Output contains the output from the command

if (cr.Errors.Count != 0)
{
    // handle errors
}

System.Reflection.Assembly a = cr.CompiledAssembly;

// party on the type here, either via reflection...
System.Type t = a.GetType("TheDynamicallyGeneratedType");

// or via a wellknown interface

在上面的代码中, LiteralSource 包含要编译的源代码。正如我所说,我通过阅读模板并填写空白来生成这个。


14
2018-03-04 03:05



这是一个有趣的方法,我从未想过编写实际的C#然后编译它。可能我最终会做什么。谢谢。 - jsimmons
你如何在磁盘上生成类? - jcolebrand
@jcolebrand - 我不清楚你在问什么,但我认为你应该发一个新问题。 - Cheeso
是的,但这显然是一个棘手的谈话。 chat.stackoverflow.com/transcript/message/2889886#2889886 我已经尝试过如何处理它,如果你想知道我有什么,也许可以参与谈话 - jcolebrand


我不确定我是否正确解释了你的问题,但你可能想看看Castle.Dynamic代理。它允许您为类和接口创建代理,然后拦截某些方法调用(接口上的任何东西和真实类上的虚拟内容)。拦截调用时,您只需查看参数并通过P-Invoke将调用转发给lua API。这是一个很棒的教程 这里


1
2018-03-04 02:12



这很有趣,我当然会调查一下。我认为从C#调用Lua方法更有用,我想从Lua调用C#方法。 - jsimmons


您可以将C#公开为COM,这将允许所有(公共)方法被调用为外部应用程序。

或者,暴露一个C#函数,该函数将调用适当的其他函数,可能硬编码为C#中的实际函数列表,或者可能使用反射。参数可能需要一个任意大小的数组。


0
2018-03-02 03:59



使用这样的反射几乎是我想避免做的事情。 - jsimmons


试着调查一下 T4。因为它本身是Visual Studio的一部分,所以您可以使用反射框架根据您的问题查找所有方法。在谷歌搜索,我相信你可以找到一些使用T4反射的人的示例代码或模板来生成包装类或方法。


0
2018-03-04 03:05



这并不能解决能否在运行时从LUA(或任何语言)动态调用任意C#函数的问题 - Roger Willcocks
您可以使用T4生成包装类,并最终获得与您在接受的答案中使用CodeDOM相同的结果。它不是要让Lua能够动态调用任何任意方法。所以我不理解你的关注和投票! - Fadrian Sudaman
“...我有兴趣能够调用任意C#方法而不让它们遵循这些约定......” - Roger Willcocks