【问题标题】:How to use a Roslyn scripting submission as an assembly in other Roslyn compilations如何在其他 Roslyn 编译中使用 Roslyn 脚本提交作为程序集
【发布时间】:2016-07-28 20:58:04
【问题描述】:

我想在另一个非脚本 Roslyn 编译中将脚本作为动态程序集重用,但我终其一生都无法弄清楚如何实现这一目标。

例如,假设我以正常方式创建脚本,然后将脚本作为程序集发送到字节流,使用类似:

var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
var compilation = script.GetCompilation().WithOptions(compilationOptions);
using (var ms = new MemoryStream())
{
    EmitResult result = compilation.Emit(ms);
    ms.Seek(0, SeekOrigin.Begin);
    assembly = Assembly.Load(ms.ToArray());
}

现在,假设我想将该程序集作为参考提供给另一个非脚本编译。我不能只使用assembly,因为没有MetadataReference.CreateFrom...() 方法支持传递实际的Assembly 实例。作为一个动态程序集,它没有位置,所以我不能使用MetadataReference.CreateFromFile()。

过去我曾成功使用MetadataReference.CreateFromStream() 处理此类事情,但当程序集表示脚本提交时,这似乎不起作用(我不知道为什么)。编译继续进行,但一旦您尝试使用提交中的类型,您就会收到如下错误:

System.InvalidCastException: [A]Foo cannot be cast to [B]Foo. Type A originates from 'R*19cecf20-a48e-4a31-9b65-4c0163eba857#1-0, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'LoadNeither' in a byte array. Type B originates from 'R*19cecf20-a48e-4a31-9b65-4c0163eba857#1-0, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'LoadNeither' in a byte array.

我猜这与提交程序集在评估与加载为字节数组时处于不同的上下文中有关。对于在以后的非脚本编译中使用脚本提交中定义的对象和方法的最佳方式,我很乐意提供任何见解或指导。

7/29 更新

我能够获得演示问题的最小重现。可以在https://github.com/daveaglick/ScriptingAssemblyReuse找到。

在生成重现时,很明显这个问题的一个重要组成部分是脚本将其中一个类的Type 传递给调用代码,然后调用代码使用该Type 来实例化一个对象的实例,然后将该实例传递到引用脚本程序集的编译中。当从宿主应用程序创建的类型的实例转换为引用编译中的类型时,会发生不匹配。当我重新阅读时,这听起来很混乱,所以希望下面的代码能让它更清晰。

以下是触发此问题的所有代码:

namespace ScriptingAssemblyReuse
{
    public class Globals
    {
        public IFactory Factory { get; set; }    
    }

    public interface IFactory
    {
        object Get();
    }

    public class Factory<T> : IFactory where T : new()
    {
        public object Get() => new T();
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            new Program().Run();
        }

        private Assembly _scriptAssembly;

        public void Run()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;

            // Create the script
            Script<object> script = CSharpScript.Create(@"
                public class Foo { }
                Factory = new ScriptingAssemblyReuse.Factory<Foo>();
                ", ScriptOptions.Default.WithReferences(MetadataReference.CreateFromFile(typeof(IFactory).Assembly.Location)), typeof(Globals));

            // Create a compilation and get the dynamic assembly
            CSharpCompilationOptions scriptCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
            Compilation scriptCompilation = script.GetCompilation().WithOptions(scriptCompilationOptions);
            byte[] scriptAssemblyBytes;
            using (MemoryStream ms = new MemoryStream())
            {
                EmitResult result = scriptCompilation.Emit(ms);
                ms.Seek(0, SeekOrigin.Begin);
                scriptAssemblyBytes = ms.ToArray();
            }
            _scriptAssembly = Assembly.Load(scriptAssemblyBytes);

            // Evaluate the script
            Globals globals = new Globals();
            script.RunAsync(globals).Wait();

            // Create the consuming compilation
            string assemblyName = Path.GetRandomFileName();
            CSharpParseOptions parseOptions = new CSharpParseOptions();
            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
                public class Bar
                {
                    public void Baz(object obj)
                    {
                        Script.Foo foo = (Script.Foo)obj;  // This is the line that triggers the exception 
                    }
                }", parseOptions, assemblyName);
            CSharpCompilationOptions compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
            string assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
            CSharpCompilation compilation = CSharpCompilation.Create(assemblyName, new[] {syntaxTree},
                new[]
                {
                    MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll")),
                    MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll")),
                    MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll")),
                    MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))
                }, compilationOptions);
            using (MemoryStream ms = new MemoryStream(scriptAssemblyBytes))
            {
                compilation = compilation.AddReferences(MetadataReference.CreateFromStream(ms));
            }

            // Get the consuming assembly
            Assembly assembly;
            using (MemoryStream ms = new MemoryStream())
            {
                EmitResult result = compilation.Emit(ms);
                ms.Seek(0, SeekOrigin.Begin);
                byte[] assemblyBytes = ms.ToArray();
                assembly = Assembly.Load(assemblyBytes);
            }

            // Call the consuming assembly
            Type barType = assembly.GetExportedTypes().First(t => t.Name.StartsWith("Bar", StringComparison.Ordinal));
            MethodInfo bazMethod = barType.GetMethod("Baz");
            object bar = Activator.CreateInstance(barType);
            object obj = globals.Factory.Get();
            bazMethod.Invoke(bar, new []{ obj });  // The exception bubbles up and gets thrown here
        }

        private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
        {
            if (_scriptAssembly != null && args.Name == _scriptAssembly.FullName)
            {
                // Return the dynamically compiled script assembly if given it's name
                return _scriptAssembly;
            }
            return null;
        }
    }
}

【问题讨论】:

  • 你能分享整个堆栈跟踪吗?以及实际导致它的代码?您描述的场景有效。另外-您如何引用脚本中的类型?他们有一个非法的标识符 (#)。
  • @EliArbel 该应用程序非常复杂,因此我试图将其提炼成更简单的复制品。我很难获得上面提到的确切 InvalidCastException ,而是在我尝试在消费编译中使用提交中的类型时获得 TypeLoadException 。我觉得这很有趣,并且可能相关。我到目前为止的复制品在这里:github.com/daveaglick/ScriptingAssemblyReuse
  • @EliArbel 我已在链接的 GitHub 存储库及以上代码中添加了示例代码
  • 您有什么理由不只是使用编译到编译的参考吗?
  • @JasonMalinowski 这实际上是我一直在做的,而且效果很好。我试图首先切换到脚本引擎,以利用它自动将内联类、方法等从脚本方法中提升到容器类中/与容器类一起使用的方式。由于这个问题,我从来没有让脚本引擎工作,但我能够通过使用 Script 源类型编译然后使用 walker 访问顶部来获得我所追求的编译到编译参考树中的节点并提取可推广的节点。

标签: c# .net .net-assembly roslyn


【解决方案1】:

我想我已经解开了这个谜。以下是发生的事情:

  • 创建一个脚本。
  • 获取脚本的编译,但覆盖编译选项。具体来说,它使用默认的ScriptClassName,即Script,而不是由脚本API 生成的(例如Submission#0) - 这是问题的症结。
  • 使用覆盖的选项发出程序集并将其从流中加载到内存中。
  • 运行脚本。此时,使用字节数组将两个不同且不兼容的同名程序集加载到AppDomain。
  • 将流作为元数据引用添加到新编辑并在代码中使用。它编译得很好,因为它使用了从覆盖选项生成的程序集。您将无法使用脚本创建的实际程序集来编译它,因为 Submission#0 之类的名称在 C# 中是非法的。如果不是,那么您可以将实际脚本 Assembly 实例放在全局变量中并在 OnAssemblyResolve 中使用它。
  • 使用Submission#0+Foo 类型的参数调用方法Baz 并尝试将其强制转换为Script+Foo。

总结 - 我认为使用当前的 Roslyn 脚本 API 是不可能的。但是,这些 API 并不是编译脚本的唯一方法。您可以自己创建一个编译并将SourceCodeKind 设置为Script。你必须自己做很多事情,比如运行主脚本方法、处理全局变量等。我做了类似in RoslynPad 的事情,因为我希望脚本程序集与 PDB 一起加载(所以异常会有行信息) .

【讨论】:

  • 啊,哈!这就说得通了。如果我理解,您是说它仅在构建编译时替换脚本容器类名称,因此当您仅评估脚本时,它内部的任何内容都包含在不同的类中(这在 C# 中也恰好是非法的)。具有讽刺意味的是,我试图将一些代码从我自己的“脚本宿主”移植到脚本引擎(主要是利用它“提升”类和方法到包含类的酷方法)。看起来我现在将回到自定义滚动脚本解决方案。感谢您的帮助!
  • 是的。这是因为您创建了一个 CSharpCompilationOptions 的新实例,它的默认值为 ScriptClassName(而不是使用脚本创建的选项)。
  • 知道了,因为我无法为普通脚本运行器提供备用 CSharpCompilationOptions,所以它们永远不会有相同的名称。再三考虑 - 甚至可能有办法解决这个问题。如果我从不使用Script&lt;T&gt;.RunAsync() 方法,而是仅使用反射从发出的程序集中评估脚本,它可能会起作用。可能需要一点脚手架,但这很容易从 Roslyn 源中复制。至少现在我知道去哪里找了。
  • 如果您不运行脚本,脚本 API 不会做更多的工作,只是创建编译,所以您最好自己做所有事情。看看RP中的ScriptRunner类,真的没那么复杂。
  • 你仍然可以拥有这一切。只需使用new CSharpParseOptions(kind: SourceCodeKind.Script)。脚本 API 只是编译器本身内置的脚本语言之上的一个薄包装器。
猜你喜欢
  • 1970-01-01
  • 2020-10-12
  • 1970-01-01
  • 2018-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-10
  • 1970-01-01
相关资源
最近更新 更多