LicenseToCIL 1.0.0

dotnet add package LicenseToCIL --version 1.0.0                
NuGet\Install-Package LicenseToCIL -Version 1.0.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LicenseToCIL" Version="1.0.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add LicenseToCIL --version 1.0.0                
#r "nuget: LicenseToCIL, 1.0.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install LicenseToCIL as a Cake Addin
#addin nuget:?package=LicenseToCIL&version=1.0.0

// Install LicenseToCIL as a Cake Tool
#tool nuget:?package=LicenseToCIL&version=1.0.0                

What is it?

This is a library that wraps the .NET System.Reflection.Emit API for generating CIL code at runtime. The wrapper adds a tiny bit of type safety by doing two things:

Dedicated function per opcode

Each CIL opcode gets its own function. This way, the function for generating the ldstr opcode takes a string. This might sound obvious, but the underlying System.Reflection.Emit API represents each opcode as an enum and there is nothing stopping you from writing generator.Emit(OpCodes.Ldstr, 27).

Type-checked stack depth

Most opcodes have a consistent stack behavior. For example, the add opcode requires the there be two numbers on the stack. It will pop the two, add them, and push the result.

License To CIL represents this stack behavior as a type, Op<'x S S, 'x S>, which indicates that there must be at least two elements on the stack beforehand, and that after adding there will be one element in their place.

The sequences of opcodes you emit must have types that "line up", or you'll get an error compiling your F# program.

For example, these snippets will compile:

cil {
    yield ldc'i4 1
    yield ldc'i4 2
    yield add
    yield ldc'i4 3
    yield add
    yield ret
} |> toDelegate<Func<int>> "myFunction1"
cil {
    yield ldc'i4 1
    yield ldc'i4 2
    yield ldc'i4 3
    yield add
    yield add
    yield ret
} |> toDelegate<Func<int>> "myFunction2"

This one will not:

cil {
    yield ldc'i4 1
    yield ldc'i4 2
    yield add
    yield add // stack underflow here!
    yield ldc'i4 3
    yield ret
} |> toDelegate<Func<int>> "myFunction3"

Note that only the number of elements on the stack is checked, not the types of those elements.

For my use case, much of the code I was generating was working with types that weren't known (or didn't even exist) until runtime, so it was more hassle than help to try to track them statically.

There is a similar project by kbattocchi that does track types.

How can I use it?

Writing CIL

The main feature of LicenseToCIL is the cil computation expresssion. You can write code in here much like if you were using a seq expression to generate a list of opcodes. You can use if/else and match expressions to generate code conditionally, as long as the stack state ends up consistent following each possible branch:

let defaultOfType (ty : System.Type) =
    cil {
        if ty.IsValueType then
            let! loc = deflocal ty
            yield ldloca loc
            yield initobj ty
            yield ldloc loc
        else
            yield ldnull
    }

The result of each cil expression is an Op<'stackin, 'stackout>, which you can treat like an opcode. That is, you can write a block of code in one cil expression, store the resulting Op in a variable, and yield it from other CIL expressions to inline that code whereever you need it.

You can find all the CIL opcodes in the LicenseToCIL.Ops module, with XML comments describing their stack transition behavior. They are mostly named identically to the CIL opcodes, except with . replaced by ', so ldc.i4 becomes ldc'i4. These are legal F# identifiers and are a bit easier to type too.

A few opcodes have been renamed to avoid collisions with F# keywords and builtins:

CIL opcode Ops module function
unbox unbox'val
and bit'and
or bit'or
xor bit'xor
not bit'not

Labels

When you need to generate code that branches or loops, you use labels. In a CIL block, you can define a new label by writing let! myLabel = deflabel. Then you mark where in your code that label should go with yield mark myLabel.

For conditional branches, the code following the label and the code following the branch instruction need to operate on the same number of stack elements. This requirement is type-checked.

Here's an example that simply decrements its argument down to 0 in a loop:

let busyLoop =
    cil {
        let! loop = deflabel
        let! exit = deflabel

        yield mark loop  // <----------------------------------------+
        yield ldarg 0    //                                          |
        yield ldc'i4 0   //                                          |
        yield ble's exit // if arg <= 0, branch to exit ----------+  |
        yield ldarg 0    //                                       |  |
        yield ldc'i4 1   //                                       |  |
        yield sub        //                                       |  |
        yield starg 0    // arg = arg - 1                         |  |
        yield br's loop  // unconditionally branch back to here --|--+
                         //                                       |
        yield mark exit  // <-------------------------------------+
        yield ret'void
    }

Locals

If your code needs a local variable, you can get one with deflocal, much like deflabel. deflocal takes a Type object for the type of the variable.

Here's an example that swaps the top two int elements on the stack.

let swapInts =
    cil {
        let! tmp1 = deflocal typeof<int>
        let! tmp2 = deflocal typeof<int>
        yield stloc tmp1
        yield stloc tmp2
        yield ldloc tmp1
        yield ldloc tmp2
    }

Executing your code

If you just want to assemble Func<_, ..., _> or Action<_, ..., _> delegates, you can use the toDelegate helper.

cil {
    yield ldarg 0
    yield ret
} |> toDelegate<Func<obj, obj>>

If you have a System.Reflection.Emit.ILGenerator, you can apply an Op<_, _> to it like so:

let writeCode (op : Op<_, _>) (il : ILGenerator) =
    op Stack.empty (IL(il))

Check out the examples in LicenseToCIL.Examples for more in depth usage.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on LicenseToCIL:

Package Downloads
Rezoom.SQL.Provider

Implements an F# type provider for SQL statements.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.0 1,856 7/4/2018
0.3.0 8,980 3/22/2017
0.2.2 3,132 2/15/2017
0.2.1 1,738 8/20/2016
0.2.0 1,563 8/20/2016
0.1.4 1,627 8/7/2016
0.1.3 1,394 8/2/2016
0.1.2 1,432 7/31/2016
0.1.1 1,363 7/31/2016
0.1.0 1,802 7/30/2016

Target .NET standard 2.0.