#CSharp [Keysharp]

Embeds C# in a script. The C# is compiled with the script, so it needs no separate build step.

#CSharp
C# members
#EndCSharp

#CSharp "File.cs"

#CSharp <Library>

Note: This is for the handful of paths where AutoHotkey-level semantics cost too much — tight numeric loops, buffer and pixel work, repeated DllCall. See Performance.

Parameters

File.cs

Type: String

Reads the C# from a file instead of from a block.

A relative path is searched for along the module search path, beginning with the directory of the file containing the directive. %A_ScriptDir%-style path variables expand here as they do in an #Include path.

The path is used as written; no extension is appended. The file contains members for the directive's scope, not a standalone namespace or containing class. This parameter is not an expression.

Library

Type: Name

Reads Library.cs using the same Lib-folder search order and underscore fallback as #Include <Library>.

For example, #CSharp <Math_Add> searches for Math_Add.cs in every Lib folder, then for Math.cs. Variable references are not allowed.

What the script can see

At module scope, a public static method becomes a script function within its declaring module, while public static fields and properties enter the module's variable store. Anything that is not public is a C# helper which the script cannot call. A module-scope public method which is not static is rejected because module imports expose static members.

Public static fields and properties can be named directly, dynamically or through an explicit import from their module. Imported aliases retain the original member's conversion, wrapping and public accessor rules. Inline C# can also read and write imported script variables by the alias name in lowercase.

[UserDeclaredName("Name")] supplies a public member's script name for direct access, dynamic access and imports. C# continues to use the declared member name. Renamed fields, properties, functions and types retain the original storage or identity, including through chained imports. The name can be a C# constant expression, including a const value or nameof. Using aliases can name the attribute, and constants can come from a referenced package.

An explicit { Name } import can request any public static module method. Add [Export] to include one in wildcard imports. The attribute is valid only on public static methods at module scope.

In a class-body block, public methods and public property accessors are script-visible. A private or protected accessor remains a C# helper, and an init accessor is read-only after construction. A field is never script-visible, but it may hold C#-side state shared by that class's blocks.

#CSharp
static long[] scratch = new long[256];    ; not public: not a script variable
static long Helper(long n) => n * 2;      ; not public: not a script function

public static long Doubled(long n) => Helper(n);
#EndCSharp

MsgBox Doubled(21)   ; 42

A name that collides with a script variable or function, or with a name Keysharp generates, is reported against the block.

Blocks in a class

A block written in a class body declares members of that class.

A static member receives its receiver as an ordinary leading parameter, before the arguments the script passes — the object for an instance member, the class itself for a class-static one. By convention it is written object @this; the name is not checked.

A public static method whose first parameter cannot hold a receiver — because there is none, or because it is a value type, a string, an array, a pointer, or a params/by-ref parameter — is reported at compile time, including one marked [Static].

The [Static] attribute, or a static prefix on the name, makes the member class-static; otherwise it is an instance member:

class Vec {
    __New(x, y) => (this.x := x, this.y := y)

    #CSharp
    ; An instance member: called as vec.Len2(scale), with vec as @this.
    public static object Len2(object @this, double scale)
    {
        var x = (double)Script.GetPropertyValue(@this, "x");
        var y = (double)Script.GetPropertyValue(@this, "y");
        return (x * x + y * y) * scale;
    }

    ; A class-static member: called as Vec.Origin().
    [Static]
    public static object Origin(object @this) => "0,0";

    ; Not public, so it needs no receiver: the script cannot call it, but this block can.
    static long Twice(long n) => n * 2;
    #EndCSharp
}

v := Vec(3.0, 4.0)
MsgBox v.Len2(2)      ; 50.0
MsgBox Vec.Origin()   ; 0,0

These members behave like script-declared methods. Ordinary parameters follow the receiver — typed, optional or params:

#CSharp
[Static]
public static long Sum3(object @this, long a, long b, long c) => a + b + c;

; The `static` name prefix instead of the attribute; the script sees Vec.Scaled().
public static long staticScaled(object @this, long n) => n * 10;

; Variadic: the receiver is its own parameter, so the array holds only the real arguments.
[Static]
public static long Count(object @this, params object[] args) => args.Length;
#EndCSharp

A class-static property is written as its accessor method, staticget_Name or staticset_Name. For example, public static long staticget_Answer(object @this) => 42; is read by the script as Vec.Answer.

A public static property in a class body is rejected, but a C# instance method or property is accepted, and declares no receiver:

#CSharp
public object Thrice(long n) => n * 3;   ; called as v.Thrice(4)
public long Half => 21;                  ; read as v.Half
#EndCSharp

A nested class gets its own block, in its own type. Non-public members are scoped to the class they were written in, so two classes may each declare a private helper of the same name without colliding.

Arguments and return values

Values crossing the boundary are converted with AutoHotkey's rules, not the CLR's, so a typed C# parameter accepts ordinary script values:

#CSharp
public static long SumSquares(long n)
{
    long acc = 0;
    for (long i = 1; i <= n; i++) acc += i * i;
    return acc;
}
#EndCSharp

MsgBox SumSquares(4)      ; 30
MsgBox SumSquares("4")    ; 30 — a numeric string converts, as "4" == 4 does
MsgBox SumSquares(4.0)    ; 30 — a Float truncates toward zero

A non-numeric value raises a TypeError, as 1 + "abc" does. Integer-family and floating-point return values become Integer and Float.

A returned CLR value which has no native script representation is wrapped with the same objects used by Clr: System.Type becomes a ManagedType, and other CLR objects and value types become a ManagedInstance. This also applies when the declared return, property or field type is object. Passing a ManagedInstance back to C# unwraps its payload for a matching reference or value type and for an object target; ManagedType similarly unwraps for a System.Type-compatible target. The Clr byte-array and byte-span conversion rules also apply at this boundary.

The exported boundary supports objects and reference types, strings, Booleans, integer-family types, float/double, Span<byte>/ReadOnlySpan<byte> and params object[]. Signatures which the script dispatcher cannot represent are rejected: ref/out/in, pointers and by-ref returns, char, decimal, nullable and tuple types, other Span<T>/ReadOnlySpan<T> types, other typed params arrays, and generic exported methods. Make such a method non-public when it is only a C# helper.

Note: A using alias for a rejected type, or a user-defined ref struct, is not detected at compile time; such a member fails when it is first called.

Sharing state with the script

C# reads and writes a module global directly, by its name in lowercase:

global Hits := 0

#CSharp
public static long Tally(long n)
{
    hits = (long)hits + 1;    ; the script's `Hits`
    return n;
}
#EndCSharp

A global holds an unconverted script value whose type the script decides, so a C# cast on it can fail. Pass the value as a typed parameter instead; it is converted at the boundary.

Namespaces and preprocessor symbols

A using at the top of any block applies to every module and class block in the same script module, and to no other module. Already in scope in each module: System, System.Collections.Generic, System.Runtime.CompilerServices, System.Runtime.InteropServices and Keysharp.Runtime.

Keysharp.Builtins is not imported: its Array, String and Buffer would be ambiguous with the System types of the same name. Name those types in full, or import the namespace and resolve the ambiguity for that module.

A using may be guarded by #if; only the active branch is imported. A global using applies only to its own module.

The symbols active at the directive's source position apply inside its block, including around using directives, so #if WINDOWS selects the same branch on both sides and a later #Define or #Undef does not change an earlier block. Imported modules retain their own symbols. Note that Keysharp matches #Define names case-insensitively while C# does not.

Pointers and unsafe code

Pointer code is allowed in code marked with C#'s unsafe keyword.

Warning: A pointer write which causes an access violation ends the process at once, with no OnError, error dialog or OnExit, as it can with DllCall and NumPut.

Errors

Syntax errors and the boundary and name checks described above point to the script or C# file. Other C# compile errors point to the generated inline C# unit.

An exception thrown inside inline C# is mapped to the matching Keysharp error before it reaches the script, so an ordinary try intercepts it:

#CSharp
public static long Boom(long i)
{
    var a = new long[2];
    return a[i];
}
#EndCSharp

try
    Boom(5)
catch IndexError
    MsgBox "caught"

The error is reported as if raised by a built-in function named after the member, and its Message names the exception, as in Boom threw IndexOutOfRangeException: Index was outside the bounds of the array.

Performance

A call into inline C# costs as much as any other function call; only the body runs faster. Put the whole loop inside the C#: a million calls into a one-line helper is slower than staying in script code.

For bulk numeric data, hand C# a Buffer rather than an Array. Array element access stays at script speed even from C#, while a Buffer's memory can be walked directly as a Span.

Remarks

All blocks in one script module form one C# syntax tree, so a later block in that module can use a type declared in an earlier one.

A block cannot appear inside a function, or inside a block such as an If body. Like other directives, #CSharp cannot be executed conditionally.

Compiled scripts carry their inline C# as ordinary compiled code, so nothing extra is needed on the machine that runs one.

--transpile writes a tooling view of the inline C# to a separate Scriptname.inline.cs beside the generated Scriptname.cs. In a multi-module script this view concatenates the module-specific units.

Clr, DllCall, #Package, Buffer, #Include

Examples

Runs a hot numeric loop in C#. The whole loop is inside, so the script pays one call.

#CSharp
public static long Collatz(long start, long limit)
{
    long best = 0, bestN = 0;
    for (long n = start; n < limit; n++)
    {
        long x = n, steps = 0;
        while (x != 1) { x = (x & 1) == 0 ? x >> 1 : 3 * x + 1; steps++; }
        if (steps > best) { best = steps; bestN = n; }
    }
    return bestN;
}
#EndCSharp

MsgBox "longest chain under 1e6 starts at " Collatz(1, 1000000)

Walks a screen capture's pixels directly, using C#'s own unsafe for the pointer.

#CSharp
public static unsafe long CountNear(Keysharp.Builtins.Buffer buf, int argb, int tol = 16)
{
    int wr = (argb >> 16) & 0xFF, wg = (argb >> 8) & 0xFF, wb = argb & 0xFF;
    var px = new System.ReadOnlySpan<byte>((void*)buf.Ptr, (int)(long)buf.Size);
    long hits = 0;
    for (int i = 0; i + 3 < px.Length; i += 4)
        if (System.Math.Abs(px[i] - wr) <= tol
         && System.Math.Abs(px[i + 1] - wg) <= tol
         && System.Math.Abs(px[i + 2] - wb) <= tol) hits++;
    return hits;
}
#EndCSharp

#Import "Ks" { Image }
Img := Image.FromRect(0, 0, A_ScreenWidth, A_ScreenHeight)
MsgBox CountNear(Img.GetPixelData(4), 0xFF3B30, 20) " reddish pixels"

Keeps the C# in its own file, where an editor can help with it.

#CSharp "signal.cs"

Peaks := FindPeaks(Samples, 0.4)