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

查看:29
本文介绍了如何将 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());
}

现在,假设我想将该程序集提供给另一个非脚本编译作为参考.我不能只使用 assembly,因为没有一个 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 传递给调用代码,然后调用代码使用 Typecode>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 中使用它.
  • 使用 Submission#0+Foo 类型的参数调用方法 Baz 并尝试将其转换为 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 Scripting 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天全站免登陆