1

In the Roslyn Scripting API, it's possible to pass values to the script as the properties of a "globals" object.

Can something similar be done when using the workspace API?

Here's my sample code:

var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
            .WithOverflowChecks(true).WithOptimizationLevel(OptimizationLevel.Release)
            .WithUsings("System", "System.Collections", "System.Collections.Generic", "System.Dynamic", "System.Linq");

string userCode = "... end user's code goes here...";

using (var workspace = new AdhocWorkspace() { })
{
    string projName = "NewProject132";
    var projectId = ProjectId.CreateNewId();
    var projectInfo = ProjectInfo.Create(
        projectId,
        VersionStamp.Create(),
        projName,
        projName,
        LanguageNames.CSharp,
        isSubmission: true,
        compilationOptions: options,
        metadataReferences: references,
        parseOptions: new CSharpParseOptions(kind: SourceCodeKind.Script, languageVersion: LanguageVersion.Latest));
    var project = workspace.AddProject(projectInfo);

    var id = DocumentId.CreateNewId(project.Id);


    /*
        how do I declare variables that are supposed to be visible to the user's code?
        */

    var solution = project.Solution.AddDocument(id, project.Name, userCode);
    var document = solution.GetDocument(id);

    //get syntax and semantic errors
    var syntaxTree = document.GetSyntaxTreeAsync().Result;
    foreach (var syntaxError in syntaxTree.GetDiagnostics())
    {
        //...
    }
    var model = document.GetSemanticModelAsync().Result;
    foreach (var syntaxError in model.GetDiagnostics(new TextSpan(0, userCode.Length)))
    {
        //...
    }
    var completionService = CompletionService.GetService(document);
    var completions = completionService.GetCompletionsAsync(document, userCode.Length - 1).Result;
}

The document is being populated with the users script, but the script needs to be able to access some values from the host application.

As a last resort, I could add the variable declarations before the user's script, but that seems a bit messy and I'd like to avoid it if possible.

anakic
  • 2,746
  • 1
  • 30
  • 32

2 Answers2

3

To give the script access to globals.

When you create the ProjectInfo set the HostObjectType to the type of the global class. You will probably also need to add a MetadataReference to the assembly that the host object type is defined in. The members of the HostObjectType will now be visible to the script.

To invoke the script with the globals.

For a script submission the compiler synthesizes a class with all the top-level methods as its members, and the top level statements logically make up the body of the constructor (but not really because of async).

Each submission also generates a method on this type that constructs the submission class and runs the body of the script. This method takes an object[] as a parameter. The first element of this array is assumed to be globals object instance. The other elements hold onto the submission class instances, in case there are multiple submissions (when used as a REPL like csi.exe).

After loading the generated assembly (created via Compilation.Emit), you can invoke this method via reflection.

Edit: You can set the name of the generated script class in the CSharpCompilationOptions.

Matt Warren
  • 1,956
  • 14
  • 15
  • Excellent answer. Specifying the HostObjectType parameter made the variables available in autocomplete and enabled proper detection of their symbols, and the reflection stuff sorted out the execution. Thanks! – anakic Oct 04 '17 at 07:53
1

Adding to Matt's answer, here's my code snippet for executing the script:

var compilation = document.GetSemanticModelAsync().Result.Compilation;
using (MemoryStream ms = new MemoryStream())
{
    var emitResult = compilation.Emit(ms);
    var assembly = Assembly.Load(ms.GetBuffer());
    Type t = assembly.GetTypes().First();

    var res = t.GetMethod("<Factory>").Invoke(null, new object[] { new object[] { Activator.CreateInstance(_customType), null } });
}

The method to call is <Factory>, it's a static method. The first parameter is my globals object. Since I have no previous submissions, I'm passing in null as the second parameter.

anakic
  • 2,746
  • 1
  • 30
  • 32