如何在其他Roslyn编译中将Roslyn脚本提交作为程序集使用 [英] How to use a Roslyn scripting submission as an assembly in other Roslyn compilations

查看:124
本文介绍了如何在其他Roslyn编译中将Roslyn脚本提交作为程序集使用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想在另一个非脚本化的Roslyn编译中将脚本作为动态程序集重用,但我一辈子都无法弄清楚如何实现该功能。

I'd like to reuse a script as a dynamic assembly in another non-scripting Roslyn compilation, but I can't for the life of me figure out how to make that work.

例如,假设我以正常方式创建了一个脚本,然后使用类似于以下内容的脚本将其作为程序集发送到字节流:

For example, say a I create a script the normal way and then emit the script as an assembly to a byte stream using something like:

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

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

Now, let's say I want to feed that assembly into another non-scripting compilation as a reference. I can't just use the assembly since none of the MetadataReference.CreateFrom...() methods support passing an actual Assembly instance. As a dynamic assembly it doesn't have a location so I can't use MetadataReference.CreateFromFile().

过去,我已经使用 MetadataReference.CreateFromStream()来成功完成这种事情,但是当程序集表示脚本提交时,这似乎不起作用(我不知道为什么)。编译继续进行,但是一旦您尝试使用提交中的类型,就会出现类似以下错误:

In the past I've used MetadataReference.CreateFromStream() for this type of thing with success, but that doesn't seem to work when the assembly represents a script submission (I have no idea why). The compilation proceeds, but as soon as you attempt to use a type from the submission you get errors like:

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.

我猜这与在评估与。作为字节数组加载。对于在以后的非脚本编译中使用脚本提交中定义的对象和方法的最佳方法,我将有任何见解或指导。

I'm guessing it has something to do with the submission assembly being in a different context when evaluating vs. loaded as a byte array. I'd love any insight or guidance into the best way to use objects and methods defined in a script submission in later non-script compilations.

更新7 / 29

我能够获得最少的再现性来证明问题所在。可以在 https://github.com/daveaglick/ScriptingAssemblyReuse 中找到。

I was able to get a minimal repro that demonstrates the problem. It can be found at https://github.com/daveaglick/ScriptingAssemblyReuse.

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

In producing the repro, it became clear that an important component of this problem is that the script passes the Type of one of it's classes out to the calling code, the calling code then uses that Type to instantiate an instance of the object, and then passes the instance into the compilation that references the script assembly. The mismatch happens when casting from the instance of the type created by the host application to the type from within the referencing compilation. As I re-read that it sounds very confusing, so hopefully the code below will make it clearer.

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

Here's all the code to trigger this problem:

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


推荐答案

我想我已经解决了这个谜。发生的情况如下:

I think I've solved the mystery. Here's what happens:


  • 创建脚本。

  • 获取脚本的编译,但覆盖编译选项。具体来说,它使用默认的 ScriptClassName ,它是 Script ,而不是脚本API生成的脚本(例如 Submission#0 )-这是问题的症结所在

  • 使用覆盖的选项& ;将其从流中加载到内存中。

  • 运行脚本。此时,使用字节数组将两个不同的且不兼容的同名程序集加载到 AppDomain 中。

  • 将该流作为元数据引用添加到新的编译中,并在代码中使用它。由于它使用从覆盖选项生成的程序集,因此可以很好地编译。您将无法使用脚本创建的实际程序集对其进行编译,因为诸如 Submission#0 之类的名称在C#中是非法的。如果不是,则可以将实际的脚本 Assembly 实例放在全局变量中,并在 OnAssemblyResolve 中使用它。 / li>
  • 调用方法 Baz ,其参数类型为 Submission#0 + Foo 和尝试将其转换为 Script + Foo

  • Create a script.
  • Get the script's compilation, but override the compilation options. Specifically, it uses the default ScriptClassName, which is Script rather than the one generated by the scripting API (e.g. Submission#0) - this is the crux of the matter.
  • Emit an assembly using the overridden options & load it into memory from the stream.
  • Run the script. At this point there are two distinct and incompatible assemblies with the same name in loaded into the AppDomain using byte arrays.
  • Add the stream as a metadata reference to a new compilation and use it in code. It compiles fine since it's using the assembly generated from overridden options. You would not be able to compile it using the actual assembly created by the script, since names like Submission#0 are illegal in C#. If it weren't then you could put the actual script Assembly instance in the globals and use it in OnAssemblyResolve.
  • Call method Baz with a parameter of type Submission#0+Foo and try to cast it to Script+Foo.

总结一下-我不知道我们相信使用当前的Roslyn脚本API不可能做到这一点。但是,这些API并不是编译脚本的唯一方法。您可以自己创建一个编译,然后将 SourceCodeKind 设置为 Script 。您必须自己做很多事情,例如运行主脚本方法,处理全局变量等。我已经做了类似的事情在RoslynPad中 ,因为我希望脚本程序集与PDB一起加载(因此异常会有行信息)。

To conclude - I don't believe this is possible using the current Roslyn Scripting API. However, these APIs are not the only way to compile scripts. You can just create a compilation on your own and set the SourceCodeKind to Script. You'd have to do a lot yourself, like running the main script method, handling globals, etc. I've done something like this in RoslynPad because I wanted the script assemblies to load with PDBs (so exceptions would have line information).

这篇关于如何在其他Roslyn编译中将Roslyn脚本提交作为程序集使用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆