Skip to content

Script Libraries

Glenn Block edited this page Feb 25, 2015 · 6 revisions

Overview

One of the key features of scripts today is that it allows you to author [Script Packs] (https://github.com/scriptcs/scriptcs/wiki/Script-Packs). These are reusable bits of functionality distributed via NuGet that generally are used to wrap existing libraries and frameworks and make them easier to consume from script. Script Packs offer the ability to inject namespaces and references into the consuming script dynamically. They can also offer a more script-friendly API that automatically handles redundant plumbing code which is generally handled by an IDE.

The biggest downside of Script Packs is the authoring. Script Packs today are .NET assemblies, this means you have to open an IDE, create a C# project, and then implement a bunch of interfaces in order to create your pack. This adds significant complexity as compared to the simplicity of writing a scriptcs "app" i.e. a set of csx scripts and a packages.config. It feels against the spirit of scriptcs in that sense.

If the experience is greatly simplified, we believe this will pave the way for the scriptcs ecosystem to really blossom.

Proposal

Offer an experience for creating reusable Script Libraries. That is APIs as scripts which can be published as packages and consumed in scriptcs similar to any other NuGet package.

  • The experience should be as close as possible to authoring a scriptcs "app" today with one significant difference is that the script is designed to be directly consumed by an app and it offers an API.
  • It should be far simpler than authoring Script Packs today:
  • No csproj is required, only scripts
  • No need to implement an IScriptPack or IScriptPackContext
  • scripts can easily leverage other script packs whether authored as binaries or scripts
  • It should be much more approachable. If you can write a function in a csx, you can author a script pack.
  • It should support the same execution guarantees as script packs do today
  • Functions are lazy / not executed until they are needed
  • Functions are isolated / do not conflict with similarly named functions in other script packs
  • It should feel more natural for C# developers
  • No need for Require<T>() to pull in the script. If you install it, you can new it up.

Goals for the initial release

  • Support C# scripts
  • Support dependencies on other packages and script packs

Non goals for the initial release

  • Support custom engines
  • Support languages other than C#

Scenarios

Authoring a basic Script Library

Priya wants to implement a Calculator library. It offers functions like Add, Subtract, Multiply and Divide.

She authors the following script:

//CalculatorMain.csx
public double Add(double a, double b) {
  ...
}

public double Subtract(double a, double b) {
  ...
}

public double Multiple(double a, double b) {
  ...
}

public double Divide(double a, double b) {
  ...
}

She tests out her script by using #load in the REPL and it appears to work fine. Now she is ready to package up her script so it can be shared. She creates the following simple folder structure.

\ScriptCs.Calculator
  \Content
    \scriptcs
      CalculatorMain.csx

Next she creates ScriptCs.Calculator.nuspec for her package specifying the required package details, i.e. name: ScriptCs.Calculator , version: 0.1, etc. Her package has no dependencies so all that is necessary is the top-level information.

Using nuget.exe she creates her package ScriptCs.Calculator.0.1.0.nupkg then copies to a folder on her local machine which she has added as one of her NuGet package sources.

Now that the package is ready to test, she goes to a new folder to create her test app. First she installs the package. scriptcs -install -pre ScriptCs.Calculator

Next she creates a start.csx file which will use the calculator.

//no require necessary for this, it is present.
var calc = new Calculator();

var result = calc.Add(5,2);
Console.WriteLine(result);

Priya runs the "app" and is happy to see it compiles successfully and runs.

Adding a library reference

Priya realizes that she add some logging to her calculator:

She goes to her nuspec and adds a reference to a the Logger script pack, ScriptCs.Logger.ScriptPack and its dependencies. Next she then updates her CalculatorMain.csx file using Require to get the package:

Logger logger = Require<Logger>();

Note: var cannot be used here for the logger this is code that will be sitting in a wrapper class so it cannot be loose code. The above code sets a class level private field logger. Require<T> in this context is a static method on the wrapper class. See the section "Scoping / Containment" below for more on the wrapper.

Priya then adds a bunch of log entries throughout her code. Next she creates a new package and puts it into her local folder which she installed from before. For example this is the Add method:

public double Add(double a, double b) {
  logger.Info(String.Format("Adding {0} + {1}", a, b));
  return a+b;
}

She goes back to her app folder and does scriptcs -install which updates the package and which pulls in the logger pack and its dependencies.

Next she reruns her test app and is happy to see the log entries showing up.

Design

Authoring

Script Libraries are normal scriptcs csx files, they support #load, #r, custom directives and require. One key constraint, it must have an "entry" level script which uses the convention [Name]Main.csx i.e. CalculatorMain.csx. This is the main entry point for the script pack.

Packaging

Script Libraries are packaged up as NuGet packages. scripts must be placed in the Content/scriptcs folder within the package. All files must be added explicitly to the nuspec. Any package dependencies should be added as for a normal package. The dependencies can be standard packages, packages containing traditional script packs, or other script based packages.

Below is an example NuSpec for the Caclulator library. It contains 2 csx files as well as a dependency on the ScriptCs.Logger.ScriptPack package which is a standard script pack.

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>ScriptCs.Calculator</id>
    <version>0.1.0</version>
    <authors>Glenn Block</authors>
    <owners>Glenn Block</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>A calculator script pack</description>
    <language>en-US</language>
    <dependencies>
      <dependency id="ScriptCs.Logger.ScriptPack" version="0.1.0" />
    </dependencies>
  </metadata>
  <files>
    <file src="CalculatorMain.csx" target="Content\scriptcs\CalculatorMain.csx" />
  </files>
</package>

Discovery / Loading

Script Libraries will be loaded in two phases at runtime.

  1. Discovery and Composition - Scriptcs will look for a ScriptLibaries.csx file in the scriptcs_packages folder. If it does not find one, it will look for any packages that have a /Content/scriptcs folder and will automatically grab all the scripts. These scripts will be wrapped in outer classes per package (See Scoping and Containment) and placed in ScriptLibraries.csx.

  2. Execution - At runtime scriptcs will look automatically merge ScriptLibraries.csx into the main file which is executes.

This model provides several benefits:

  • Runtime execution should be fast.
  • File IO is minimized at runtime as once the script packages have been processed they will not need to get re-processed each time.
  • Roslyn does not have to be invoked additionally for each scripted library.
  • They can be cached as they naturally work with the existing caching infra.
  • No need for Require as the script is already present.
  • Packs can depend on each other.

One key constraint this introduces is that binary script packs cannot depend on Script Libraries. This might be able to be addressed in the future but this is TBD.

Scoping / Containment

One key challenge with this approach is it could make naming collisions likely. Also there is an issue of scoping. In the .NET proper world, namespaces would be a way to achieve this, but in Roslyn this is not a possibility as you can't have namespaces in script as they execute within an outer class.

To remove collisions and introduce some form of containment, we will use an outer class. For each script library, all of it's contents will automatically be wrapped in an outer class. The name for the class will be the name of the Main csx file minus "Main".

Using the Calculator example the code will be embedded in a Calculator class.

public class Calculator : ScriptLibraryWrapper {
  public double Add(double a, double b) {
    ...
  }

  public double Subtract(double a, double b) {
    ...
  }

  public double Multiple(double a, double b) {
    ...
  }

  public double Divide(double a, double b) {
    ...
  }
}

ScriptLibraryWrapper is a base class for the wrapper. It's main purpose is to surface up a static Require<T> method which the script can use to get access to script packs.

Lifting of directives and usings

A challenge posed by this model of containment is around usings and directives. Currently in scriptcs we expect all usings and directives to be at the top of the file when we parse it (using the processor). As we are wrapping the entire contents of the library, this can pose a problem as the scripts will likely have usings and directives which will be contained within which will cause the file processor to fail.

The way this will be addressed is to walk through all package scripts and pass them through the pre-processor. As each script is processed it will be appended into a script in memory, while all the usings are saved. After all files are processed then the usings will be appended at the top of the file.

Extensibility / additional languages

All ScriptLibrary functionality is implemented in the ScriptLibraryComposer and currently only supports C#. If one wants to extend the functionality, for example to allow FSharp module users to include Script Libraries authored as .fs files, then the module would need to register a custom composer. The composer exposes 2 methods, one which returns the composed ScriptLibrary filename and the other which creates wrapper classes and creates the ScriptLibrary file.

This file is automatically loaded and its contents are appended to the end of the executing script which in this case would be written in F#. If the default append behavior is not acceptable for that language, then a custom ScriptExecutor should be provided and the InjectScriptLibraries virtual method should be overridden with the appropriate logic.

You can’t perform that action at this time.