Skip to main content

A general-purpose, embeddable scripting language for .NET with C# syntax, compiled execution, low-allocation runtime paths, and host-controlled access.

Total Downloads
872
872
Published
Oct 22, 2026
today

Get Started

dotnet add package Facepunch.Breen --version 1.0.0-rc.15
ProductVersions
.NET
net10.0

README

Breen

Breen is a scripting language for C# applications. It looks and works like C#, because that's the idea.

Use the Facepunch.Breen package with .NET 10. Scripts run as compiled code, with inputs and C# APIs supplied by your application.

Example script

Save this as score.breen:

// A global keeps its value between runs. This initializer runs once.
[Global]
int TotalScore = 0;

// Define a struct to keep a round's data and scoring logic together.
struct RoundScore
{
    public int Kills;
    public int Bonus;

    // Structs can have methods too.
    public int Total() => Kills * 100 + Bonus;
}

// Define a function we can call for each round.
int AddRound(int kills, int bonus)
{
    // This local struct belongs to this call.
    var round = new RoundScore { Kills = kills, Bonus = bonus };

    // Call its method and add the result to our running total.
    TotalScore += round.Total();
    return TotalScore;
}

// Kills and Bonus are inputs supplied by the application.
// With 3 kills and a bonus of 5, each run adds 305 to the total.
return AddRound(Kills, Bonus);

Quick start

dotnet add package Facepunch.Breen --prerelease
using Breen;

var system = new ScriptSystem(new ScriptSystemOptions
{
    Resolver = new DefaultResolver()
});

system.SetStore("global", new ScriptStore());

var script = system.CreateScript(File.ReadAllText("score.breen"));
script.Set("Kills", 3);
script.Set("Bonus", 5);

var result = script.Run();
if (!result.Success)
{
    throw new InvalidOperationException(result.Error!.Message, result.Exception);
}

int score = script.ReturnValue.Get<int>(); // 305

[Breen.Store("global")]
[AttributeUsage(AttributeTargets.Field)]
public sealed class GlobalAttribute : Attribute { }

Keep the system and script around for repeated runs; Run() compiles on first use. Change inputs with Set() and run again. Use them sequentially on one thread.

DefaultResolver exposes public C# APIs. Supply your own IResolver to restrict which types and members scripts can access.

Temporary inputs

Use With() to supply values for an invocation without keeping them on the script. Chain multiple inputs, including ref struct values:

var invocation = script
    .With("writer", writer)
    .With("reader", reader)
    .With("amount", 10);

var compilation = invocation.Compile(); // Optional; does not execute the script.
var result = invocation.Run(instructionLimit: 1_000);

With() returns a new invocation, so keep its result or chain it directly into Run(). Names must be referenced by the script, and the last value supplied for a name wins. Inputs assigned with Set() remain available; temporary values override them during execution and the original values are restored even if execution fails.

The invocation is a ref struct and follows C# lifetime rules. Values are copied for each run: borrowed buffers and host objects remain shared, while mutations to a supplied struct's own fields affect only its execution copy. Temporary input assignments are discarded; ordinary values explicitly returned or stored by the script follow the usual persistence rules.

Stack-only values cannot be boxed, returned, or stored for later execution. Scripts receiving stack-only inputs currently cannot declare local functions or perform nested struct assignments through those inputs. Use ordinary With() inputs or Set() inputs when those features are needed.

Features

βœ… Use classes βœ… Define classes βœ… Use structs βœ… Define structs
βœ… Use methods βœ… Define methods βœ… Use enums βœ… Define enums
βœ… Locals βœ… Globals βœ… Loops βœ… Arrays
βœ… Constructors βœ… Overloads βœ… Recursion βœ… Try catch
βœ… Static methods βœ… Public/private fields βœ… Collections βœ… String interpolation
βœ… Use generic classes βœ… Use generic structs βœ… Use generic methods βœ… Sandboxed APIs
βœ… Use properties βœ… Use indexers βœ… Use interfaces βœ… Execution budgets
βœ… Null checks βœ… Persistent state βœ… Editor completion βœ… Calls from C#
πŸ”΄ Define generic classes πŸ”΄ Define generic structs πŸ”΄ Define generic methods πŸ”΄ Define interfaces
πŸ”΄ Inheritance πŸ”΄ Define properties πŸ”΄ Define indexers πŸ”΄ Define operators
πŸ”΄ Define static fields πŸ”΄ Define readonly fields βœ… Lambdas πŸ”΄ Async/await
πŸ”΄ Define records πŸ”΄ Define nested types πŸ”΄ Switch πŸ”΄ Named arguments

Script.Call() invokes a top-level function and keeps initialized script state between calls. Run() reruns top-level setup. To call an object's method from C#, wrap it in a top-level script function.

Execution has a configurable instruction budget and a 128-call recursion limit. Unused function and type declarations preserve the normal execution fast paths.

Host-callable script classes

A resolver can implement IScriptTypeResolver to permit inheritance from selected compiled host classes. It must explicitly supply an accessible parameterless base constructor and any protected extension points through GetBaseMembers. Public type visibility alone does not permit inheritance. GetMarkerInterfaces can supply empty host subscription interfaces.

var script = system.CreateScript("""
    class Spinner : HostComponent
    {
        public float Speed { get; set; } = 45.0f;
        protected override void OnUpdate() { Rotation += Speed; }
    }
    """);
using var registration = script.ExportTypes(instructionLimit: 10_000);
var component = (HostComponent)Activator.CreateInstance(registration.Types.Single())!;
component.Tick(); // The host dispatches OnUpdate through the normal CLR vtable.

The emitted class has real CLR inheritance, fields and reflected get/set auto-properties. Its CLR methods bridge into the same budgeted compiled script bodies used by script calls. Host construction runs the real base constructor, then budgeted script field/property initializers and constructor code. Registration validates metadata and bodies without running user initializers. Cold function bodies emit IL on first use. Each host-constructed object owns independent execution storage. Optional entry/exit callbacks on ExportTypes let the host apply additional context guards. Dispose the registration to revoke construction and callbacks. Calls are thread-affine.

Nested host callbacks share the root instruction counter, including callbacks into another override on the same object. A scoped runtime pointer borrows the root call's stack counter only until its finally block returns; nested callbacks never retain that pointer. Reentrant callbacks borrow separate scratch storage, with the same recursion bound as script calls. Warmed parameterless callbacks allocate no managed memory (covered by HostTypeTests and HostComponentBenchmarks).

Classes may inherit another class in the same source, including forward declarations, or an approved CLR base. Use sealed to prevent inheritance. Explicit base/this constructor arguments, computed properties and generic declarations are not supported. Properties require get/set accessors with matching access. Class/field/property metadata accepts approved attribute constructors with positional literals and optional parameters. Static script methods and structs remain script-only. Source edits create new CLR types; the embedding host is responsible for replacing its registered types and instances.

During migration, ScriptTypeHost.TryInitializeField(instance, memberName) runs just that field or auto-property's initializer on an instance of the new type. This lets the host preserve existing state and initialize newly added storage without rerunning constructors. It uses the same execution budget, permissions and entry/exit guards as other host calls.

Live modules

ScriptModule separates mutable member definitions and instance storage from stable CLR adapters:

using var module = new ScriptModule(system, instructionLimit: 10_000);
module.Reload(source);
var type = module.Types.Single();
var component = (HostComponent)Activator.CreateInstance(type)!;
module.Reload(editedSource);
component.Tick(); // Same object and Type, current method body.
var properties = module.GetMembers(type).OfType<PropertyInfo>();

Reload compiles and validates the candidate before publishing. Compatible fields retain their values; new fields initialize on first access under the host budget. Changed field types receive fresh storage. Multiple classes and shared helper objects retain identity. CLR method delegates continue through the same bridge. Hosts should refresh inspector/serialization metadata from GetMembers, since CLR reflection cannot gain new properties after a type is created. Normal parameterless callbacks remain allocation-free after warmup. The standalone ExportTypes API remains available with ordinary CLR storage.

Live modules accept script bases in the same module and compiled bases. Inherited properties share their base member definition and retain independent per-instance storage. Base edits update derived instances in place. Changing a base or sealed modifier requires recreating the module; value-type declarations belong in compiled code. Removed methods report SCR_MEMBER_REMOVED; removed types revoke their callbacks. Existing script closures are not rewritten. Optional Reload validation lets the host reject names that conflict with other modules before publication. Failed compilation or validation leaves the previous definitions running.

For source-level errors, pass ScriptDocument.Parse(source, filePath) to CreateScript or ScriptModule.Reload. ScriptStackTrace.GetFrames(exception) returns script methods and their file, line, column and source span, innermost first. ScriptStackTrace.FormatException(exception) prints these before the original CLR exception using standard clickable in file:line N entries; it returns null for failures without a script trace. Tracing preserves exception types and identity, allocates only on failure, and uses the source version that executed.

Downloads by Version

Similar & Alternative Packages

Hand-picked matching NuGet libraries based on tags & ecosystem

Depended On By

Find packages that declare this package as a dependency. NuGet does not provide a reverse-dependency endpoint, so candidates are checked on demand.

Adoption Assessment

Assess this package against its supported frameworks, license, maintenance signals and dependency graph before making it a project standard.

This version is marked prerelease. Validate its API and upgrade path before production use.Compatibility metadata lists 1 target framework; confirm the exact target used by your application.

Related Guides & Workflows

Learn how to evaluate, integrate, or audit this package in your .NET project.