Skip to content

Host Integration API

Roger Johansson edited this page Jan 14, 2026 · 1 revision

Host Integration API

How to embed Asynkron.JsEngine in your .NET application and bridge between C# and JavaScript.


Overview

flowchart LR
    subgraph DotNet[".NET Host"]
        Host["C# Code"]
        Task["Task<T>"]
    end
    
    subgraph Engine["JsEngine"]
        Global["Global Object"]
        EventLoop["Event Loop"]
        Microtasks["Microtask Queue"]
    end
    
    subgraph JS["JavaScript"]
        Script["JS Code"]
        Promise["Promise"]
    end
    
    Host -->|"SetGlobalValue"| Global
    Host -->|"SetGlobalFunction"| Global
    Host -->|"Evaluate"| Script
    Task -->|"CreatePromiseFromTask"| Promise
    Script -->|"call host fn"| Host
    Promise -->|"await"| Script
Loading

Creating an Engine

// Basic creation
await using var engine = new JsEngine();

// With options
await using var engine = new JsEngine(new JsEngineOptions
{
    Logger = logger,              // ILogger for debug output
    DebugMode = true,             // Enable verbose logging
    ExecutionTimeout = TimeSpan.FromSeconds(30)  // Timeout for long-running scripts
});

Evaluation Methods

Evaluate (Async with Event Loop)

The primary evaluation method. Schedules code on the event loop, supports Promises, timers, and async/await:

// From source string
var result = await engine.Evaluate("1 + 2");  // Returns 3

// Pre-parsed for repeated execution
var program = engine.ParseProgram("x * 2");
var result1 = await engine.Evaluate(program);  // Parse once, run many times
var result2 = await engine.Evaluate(program);

EvaluateAndAwait (Drain Microtasks)

Waits for all microtasks to complete, then returns the final value of a trailing identifier:

var result = await engine.EvaluateAndAwait(@"
    let finalResult = 0;
    Promise.resolve(42).then(x => { finalResult = x; });
    finalResult;
");
// Returns 42 (not 0) - microtasks drained before reading finalResult

EvaluateSync (No Event Loop)

Fast synchronous evaluation for code without async features:

// Much faster - no event loop overhead
var result = engine.EvaluateSync("Math.sqrt(16)");  // Returns 4

// WARNING: Does NOT support:
// - setTimeout/setInterval
// - Promise resolution
// - async/await

EvaluateModule (ES Modules)

Evaluates code as an ES module with import/export support:

var exports = await engine.EvaluateModule(@"
    export const PI = 3.14159;
    export function double(x) { return x * 2; }
");

Exposing Values to JavaScript

SetGlobalValue

Expose any .NET value as a global variable:

// Primitives
engine.SetGlobalValue("appVersion", "1.0.0");
engine.SetGlobalValue("maxRetries", 3);
engine.SetGlobalValue("debugEnabled", true);

// Objects (converted to JsObject)
engine.SetGlobalValue("config", new Dictionary<string, object>
{
    ["apiUrl"] = "https://api.example.com",
    ["timeout"] = 5000
});

// Access in JavaScript
await engine.Evaluate("console.log(appVersion)");  // "1.0.0"
await engine.Evaluate("console.log(config.apiUrl)");  // "https://api.example.com"

Exposing Functions to JavaScript

Handler Delegate Types

// Simple handler - just arguments
public delegate JsValue JsSimpleHandler(IReadOnlyList<JsValue> args);

// Host handler - receives 'this' binding
public delegate JsValue JsHostHandler(JsValue thisValue, IReadOnlyList<JsValue> args);

// Async variants - return Task<JsValue>, automatically bridged to Promise
public delegate Task<JsValue> JsAsyncSimpleHandler(IReadOnlyList<JsValue> args);
public delegate Task<JsValue> JsAsyncHostHandler(JsValue thisValue, IReadOnlyList<JsValue> args);

SetGlobalFunction (Sync)

// Simple - no 'this' binding
engine.SetGlobalFunction("add", args =>
{
    var a = args[0].AsDouble();
    var b = args[1].AsDouble();
    return a + b;
});

// With 'this' binding
engine.SetGlobalFunction("greet", (thisValue, args) =>
{
    var name = args[0].AsString();
    return $"Hello, {name}!";
});

// Usage in JavaScript
await engine.Evaluate("add(2, 3)");      // Returns 5
await engine.Evaluate("greet('World')"); // Returns "Hello, World!"

SetGlobalAsyncFunction (Async -> Promise)

Automatically bridges .NET Tasks to JavaScript Promises:

// Simple async
engine.SetGlobalAsyncFunction("fetchData", async args =>
{
    var url = args[0].AsString();
    var data = await httpClient.GetStringAsync(url);
    return (JsValue)data;
});

// With 'this' binding
engine.SetGlobalAsyncFunction("delay", async (thisValue, args) =>
{
    var ms = (int)args[0].AsDouble();
    await Task.Delay(ms);
    return JsValue.Undefined;
});

// Usage in JavaScript
await engine.Evaluate(@"
    const data = await fetchData('https://api.example.com/data');
    console.log(data);
    
    await delay(1000);  // Wait 1 second
");

Task/Promise Bridging

CreatePromiseFromTask

Convert any .NET Task to a JavaScript Promise:

// With type converter
public JsValue CreatePromiseFromTask<T>(Task<T> task, Func<T, JsValue> converter)

// Example: File reading
engine.SetGlobalFunction("readFile", args =>
{
    var path = args[0].AsString();
    var task = File.ReadAllTextAsync(path);
    return engine.CreatePromiseFromTask(task, content => (JsValue)content);
});

// Usage in JavaScript
await engine.Evaluate(@"
    const content = await readFile('data.json');
    const data = JSON.parse(content);
");

Event Loop Integration

ScheduleTask

Schedule work on the engine's event loop:

engine.ScheduleTask(() =>
{
    // This runs on the JS event loop thread
    // Safe to interact with JS objects
});

ScheduleAfterTask

Schedule a continuation after an external async operation:

engine.ScheduleAfterTask(
    someExternalTask,
    () =>
    {
        // Runs on event loop after task completes
        engine.SetGlobalValue("taskResult", result);
    }
);

Module Loading

SetModuleLoader

Customize how modules are loaded:

// Simple loader - path to source
engine.SetModuleLoader(path =>
{
    // Custom resolution logic
    var fullPath = Path.Combine(baseDir, path);
    return File.ReadAllText(fullPath);
});

// Advanced loader - with referrer context
engine.SetModuleLoader((path, referrer) =>
{
    // referrer is the importing module's path
    var resolved = ResolveRelative(path, referrer);
    return File.ReadAllText(resolved);
});

// Usage in JavaScript
await engine.EvaluateModule(@"
    import { helper } from './utils.js';  // Calls your loader
    helper();
");

Complete Example

await using var engine = new JsEngine();

// Expose configuration
engine.SetGlobalValue("config", new Dictionary<string, object>
{
    ["apiBase"] = "https://api.example.com",
    ["timeout"] = 5000
});

// Expose sync utility
engine.SetGlobalFunction("log", args =>
{
    Console.WriteLine(args[0].AsString());
    return JsValue.Undefined;
});

// Expose async API
engine.SetGlobalAsyncFunction("fetchJson", async args =>
{
    var url = $"{config.apiBase}{args[0].AsString()}";
    var json = await httpClient.GetStringAsync(url);
    return (JsValue)json;
});

// Custom module loader
engine.SetModuleLoader(path => File.ReadAllText($"./scripts/{path}"));

// Run application
await engine.EvaluateModule(@"
    import { processData } from './processor.js';
    
    log('Starting...');
    
    const data = await fetchJson('/users');
    const result = processData(JSON.parse(data));
    
    log(`Processed ${result.count} items`);
");

JsValue Conversions

.NET to JsValue

JsValue value;

// Implicit conversions
value = 42;                    // Number
value = 3.14;                  // Number
value = true;                  // Boolean
value = "hello";               // String
value = someJsObject;          // Object

// Explicit
value = JsValue.FromDouble(42.0);
value = JsValue.Undefined;
value = JsValue.Null;

JsValue to .NET

JsValue value = await engine.Evaluate("someExpression");

// Type checking
if (value.IsNumber) { ... }
if (value.IsString) { ... }
if (value.IsObject) { ... }
if (value.IsNullish) { ... }  // null or undefined

// Extraction
double num = value.AsDouble();
string str = value.AsString();
bool flag = value.AsBoolean();

// Safe extraction with TryGet
if (value.TryGetDouble(out var d)) { ... }
if (value.TryGetString(out var s)) { ... }
if (value.TryGetObject<JsArray>(out var arr)) { ... }
if (value.TryGetCallable(out var func)) { ... }

Error Handling

try
{
    await engine.Evaluate("throw new Error('Something went wrong')");
}
catch (ThrowSignal signal)
{
    // JavaScript throw statement
    var errorValue = signal.ThrownValue;
    if (errorValue.TryGetObject<JsObject>(out var errorObj))
    {
        errorObj.TryGetProperty("message", out var message);
        Console.WriteLine($"JS Error: {message.AsString()}");
    }
}
catch (SyntaxException ex)
{
    // Parse error
    Console.WriteLine($"Syntax Error: {ex.Message}");
}
catch (OperationCanceledException)
{
    // Timeout or cancellation
    Console.WriteLine("Script execution timed out");
}

See Also

Clone this wiki locally