18 · Módulos
C# com extern
Funções tipadas, closures e objetos .NET chamados como código Zolo.
Nesta página
extern usa um provider importado explicitamente. No plugin dotnet, esse
provider é csharp. Os parâmetros e o retorno seguem a sintaxe Zolo; o corpo
segue a linguagem do provider.
Declare uma função C#, chame-a normalmente e passe uma closure Zolo como
callback. Uma função C# também pode devolver uma closure com estado. int
preserva os 64 bits; extern csharp async fn retorna um Future que participa
do scheduler Zolo e dos combinadores de Promise.
// expected:
// 42
// 9223372036854775807
// Zolo
// 11
// 12
// 42
// 40
// 42
use plugin dotnet::{csharp}
use std::async::Promise
extern csharp fn add(a: int, b: int) -> int {
return a + b;
}
extern csharp fn exact(value: int) -> int {
return value;
}
extern csharp fn counter(start: int) -> fn() -> int {
var value = start;
return () => System.Threading.Interlocked.Increment(ref value);
}
extern csharp fn apply(value: int, transform: fn(int) -> int) -> int {
return transform(value);
}
extern csharp async fn twice(value: int) -> int {
await System.Threading.Tasks.Task.Delay(10);
return value * 2;
}
print(add(20, 22))
print(exact(9223372036854775807))
let original = " Zolo "
let clean: str = extern csharp {
return original.Trim();
}
print(clean)
let next = counter(10)
print(next())
print(next())
print(apply(20) { |value| add(value, 22) })
let results = await Promise.all([twice(20), twice(21)])
print(results[0])
print(results[1])
Um bloco de expressão captura as variáveis Zolo referenciadas pelo corpo C#. A anotação da variável fornece o tipo esperado do resultado:
use plugin dotnet::{csharp}
let original = " Zolo "
let clean: str = extern csharp {
return original.Trim();
}
print(clean)
Para uma função pequena, coloque os imports nativos C# no começo do corpo:
extern csharp fn byte_count(text: str) -> int {
using System.Text;
return Encoding.UTF8.GetByteCount(text);
}
Use namespaces, aliases e using static em funções ou blocos de expressão.
Cada binding tem seus próprios nomes importados. Os imports vêm antes dos
comandos executáveis; using var, using (...) e await using de recursos
mantêm o escopo e o descarte normais do C#. O editor entende os imports e o corpo.
// Native imports belong at the start of the foreign fragment.
use plugin dotnet::{csharp}
extern csharp fn byte_count(text: str) -> int {
using System.Text;
return Encoding.UTF8.GetByteCount(text);
}
// The same alias can mean something different in another binding.
extern csharp fn alias_bytes(text: str) -> int {
using Native = System.Text.Encoding;
return Native.UTF8.GetByteCount(text);
}
extern csharp fn alias_abs(value: int) -> int {
using Native = System.Math;
return Native.Abs(value);
}
extern csharp async fn later(value: int) -> int {
using static System.Math;
using Tasks = System.Threading.Tasks.Task;
await Tasks.Delay(1);
return Abs(value);
}
extern csharp async fn alias_namespace() -> int {
using System = global::System.Collections.Generic;
await global::System.Threading.Tasks.Task.Delay(1);
return new System::List<int> { 42 }[0];
}
// Resource usings stay in the method and dispose at their ordinary scope.
extern csharp fn resource_declaration() -> int {
using System.IO;
using var stream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
return stream.Length;
}
extern csharp fn resource_statement() -> int {
using Stream = System.IO.MemoryStream;
using (var stream = new Stream(new byte[] { 1, 2, 3, 4 })) {
return stream.Length;
}
}
extern csharp async fn async_resource() -> int {
using System.IO;
await using var stream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
return stream.Length;
}
print(byte_count("Zolo"))
print(alias_bytes("Zolo"))
print(alias_abs(-42))
print(await later(-42))
print(await alias_namespace())
print(resource_declaration())
print(resource_statement())
print(await async_resource())
let original = " Zolo "
let clean: str = extern csharp {
using Regex = System.Text.RegularExpressions.Regex;
return Regex.Replace(original, @"\s", "");
}
print(clean)
extern csharp {
fn grouped(value: int) -> int {
using static System.Math;
return Abs(value);
}
}
print(grouped(-42))
// expected:
// 4
// 4
// 42
// 42
// 42
// 4
// 4
// 4
// Zolo
// 42
extern csharp mod expõe os tipos públicos de um módulo C#. Construtores,
métodos e propriedades recebem bindings tipados a partir dos metadados .NET.
Os objetos mantêm sua identidade e seu estado; devolvê-los por um método não
copia o objeto.
Construa um contador, altere uma propriedade, escolha overloads por tipo e
assine um evento com uma closure Zolo. Feche a assinatura quando terminar e
chame Dispose para liberar o recurso de forma determinística.
// expected:
// 5
// 41
// true
// 42
// true
// true
// 43
// number:7
// text:Zolo
// 44
// 44
// 45
// true
// 42
// i32
// i32
// int
// int
// 42
// ok
use plugin dotnet::{csharp}
extern csharp mod counters {
public sealed class Counter : IDisposable {
public long Value { get; set; }
public bool Disposed { get; private set; }
public event Action<long>? Changed;
public Counter(long start = 5) { Value = start; }
public long Next() {
if (Disposed) throw new ObjectDisposedException(nameof(Counter));
var value = ++Value;
Changed?.Invoke(value);
return value;
}
public Counter Self() => this;
public Counter? Optional(bool present) => present ? this : null;
public Counter[] Many() => new[] { this };
public string Format(long value) => "number:" + value;
public string Format(string value) => "text:" + value;
public void Dispose() { Disposed = true; Changed = null; }
}
public sealed class Keeper {
private readonly Func<long, long> callback;
public Keeper(Func<long, long> callback) { this.callback = callback; }
public long Run() {
GC.Collect();
GC.WaitForPendingFinalizers();
return callback(41);
}
}
public sealed class Pick {
public string Kind;
public Pick(int value) { Kind = "i32"; }
public Pick(long value) { Kind = "int"; }
public string Get(int value) => "i32";
public string Get(long value) => "int";
public static string Static(int value) => "ok";
}
public class Base {
public long Answer() => 42;
}
public sealed class Derived : Base { }
}
let counter = counters.Counter()
print(counter.Value)
counter.Value = 40
print(counter.Next())
let same = counter.Self()
print(same == counter)
print(same.Next())
print(counter.Optional(false) == nil)
print(counter.Optional(true) == counter)
print(counter.Many()[0].Next())
print(counter.Format(7))
print(counter.Format("Zolo"))
let subscription = counter.on_Changed(|value: int| print(value))
print(counter.Next())
subscription.close()
print(counter.Next())
counter.Dispose()
print(counter.Disposed)
let keeper = counters.Keeper(|value: int| value + 1)
print(keeper.Run())
let narrow_value: i32 = 1
let narrow = counters.Pick(narrow_value)
print(narrow.Kind)
print(narrow.Get(narrow_value))
let wide = counters.Pick(1)
print(wide.Kind)
print(wide.Get(1))
print(counters.Derived().Answer())
print(counters.Pick.Static(narrow_value))
C# aguarda callbacks que retornam Future, enquanto Zolo aguarda Task .NET.
Use try/catch nos dois sentidos e cancele uma operação em andamento com
Future.cancel(). No corpo C#, cancellation é o token cooperativo da operação.
// expected:
// 42
// 43
// callback caught
// CLR caught
// true
use plugin dotnet::{csharp}
use std::async::Promise
extern csharp async fn transform(value: int, callback: fn(int) -> Future<int>) -> int {
return await callback(value);
}
extern csharp async fn fail() -> int {
await Task.Delay(5);
throw new InvalidOperationException("outer", new ArgumentException("inner"));
}
extern csharp async fn wait_for_cancel() -> int {
await Task.Delay(60000, cancellation);
return 0;
}
async fn increment(value: int) -> int {
sleep 2ms
return value + 1
}
async fn reject(value: int) -> int {
sleep 1ms
panic("callback failed")
return value
}
async fn cancel_soon(future: Future<int>) -> bool {
sleep 5ms
future.cancel()
return future.is_done()
}
let values = await Promise.all([
transform(41, increment),
transform(42, increment),
])
print(values[0])
print(values[1])
let callback_result = try { await transform(0, reject) } catch error { "callback caught" }
print(callback_result)
let clr_result = try { await fail() } catch error { "CLR caught" }
print(clr_result)
let pending = wait_for_cancel()
print(await cancel_soon(pending))
Arquivos .cs, assemblies .dll e projetos .csproj usam a mesma entrada:
extern csharp from "./Counter.cs" use {Counter}
extern csharp from "./Legacy.dll" use Acme::Tools::{Formatter}
extern csharp from "./Services/Services.csproj" use Acme::Services::{Client}
extern csharp use System::Text::{StringBuilder}
Passe structs como DTOs tipados, preserve arrays vazios e slots nulos, e transfira bytes. Os testes também verificam inteiros de 64 bits e floats.
// expected:
// 9223372036854775807
// true
// 3
// 7
// true
// 9
// 0
// 3
// Zolo
// 42
// Hi Zolo
// Zolo
// true
// Double
use plugin dotnet::{csharp}
// An unrelated generic must not become an invalid DTO in every extern body.
struct Box<T> { value: T }
struct Payload { value: int, optional: int?, items: [int] }
extern csharp {
fn echo(input: Payload) -> Payload { return input; }
fn nullable(values: [int?]) -> [int?] { return values; }
fn empty() -> [int] { return Array.Empty<long>(); }
fn create_builder() -> Any { return new System.Text.StringBuilder(); }
}
let copy = echo(Payload {
value: 9223372036854775807,
optional: nil,
items: [7, 8, 9],
})
print(copy.value)
print(copy.optional == nil)
print(copy.items.len())
let values = nullable([7, nil, 9])
print(values[0])
print(values[1] == nil)
print(values[2])
print(empty().len())
print(nullable([7, nil, nil]).len())
let original = "Zolo"
let clean: str = extern csharp { return original.Trim(); }
print(clean)
let run = |value: int| extern csharp { return value + 1; }
print(run(41))
let greeting: str = extern csharp { return $"Hi {original}"; }
print(greeting)
// The dynamic API uses a live proxy for types absent from a typed import.
let builder = create_builder()
builder.Append("Zolo")
print(builder.ToString())
extern csharp fn is_negative_zero(value: float) -> bool {
return double.IsNegativeInfinity(1.0 / value);
}
extern csharp fn clr_type(value: Any) -> str { return value.GetType().Name; }
print(is_negative_zero(-0.0))
print(clr_type(1.0))
Importe código C#, uma DLL, um projeto com dependências e tipos do framework. No build, os artifacts necessários acompanham o executável; os fontes e o SDK não precisam estar presentes na máquina que o executa.
// expected:
// i32
// i32
// int
// 42
// 42
// 42
// number:7
// text:Zolo
// 42
// 42
// Zolo
use plugin dotnet::{csharp}
extern csharp from "./extern-fixtures/Source.cs" use Demo::Sources::{Calculator}
extern csharp from "./extern-fixtures/Legacy/bin/Release/net8.0/Legacy.dll" use Demo::Legacy::{Formatter}
extern csharp from "./extern-fixtures/Services/Services.csproj" use Demo::Services::{Client}
extern csharp use System::Text::{StringBuilder}
extern csharp fn read(client: Client) -> int { return client.Compute(40); }
extern csharp from "./extern-fixtures/Legacy/bin/Release/net8.0/Legacy.dll" fn number(value: int) -> str = Demo.Legacy.Formatter.Format
let narrow_value: i32 = 41
let narrow = Calculator(narrow_value)
print(narrow.Kind)
print(narrow.Get(narrow_value))
print(narrow.Get(1))
print(narrow.Add(1))
print(await narrow.Later(1))
let work = narrow.CreateWork()
print(await work(1))
print(number(7))
print(Formatter.Format("Zolo"))
let client = Client()
print(client.Compute(40))
print(read(client))
let builder = StringBuilder()
builder.Append("Zolo")
print(builder.ToString())
Para rodar esse exemplo diretamente, compile primeiro a DLL de demonstração:
dotnet build examples/features/14-modules/extern-fixtures/Legacy/Legacy.csproj -c Release.
Um timer .NET pode acordar um receiver Zolo parado num channel. Erros nos
callbacks passam pelo try/catch; Dispose encerra o recurso e permite
detectar um channel sem nenhuma possibilidade de progresso.
// expected:
// 42
// 43
// timer finished
// 42
// 42
// timer caught
// disposed timer
// 42
use plugin dotnet::{csharp}
// The CLR retains these callbacks after start() returns. Their bodies still
// execute on the owning Zolo runtime thread, including the async callback.
extern csharp fn start(callback: fn(int) -> nil) -> Any {
return new System.Threading.Timer(_ => callback(42), null, 20, -1);
}
extern csharp fn start_async(callback: fn(int) -> Future<int>) -> Any {
return new System.Threading.Timer(async _ => await callback(42), null, 40, -1);
}
async fn increment(value: int) -> int {
sleep 1ms
print(value + 1)
return value + 1
}
let first = start(|value| { print(value) })
let second = start_async(increment)
sleep 100ms
first.Dispose()
second.Dispose()
print("timer finished")
// A retained CLR callback is a possible waker even when all Zolo tasks
// are parked on a channel and there are no Zolo timers left to run.
let scoped = channel(1)
let scoped_timer = start(|value| { scoped.send(value) })
scope {
spawn { print(scoped.recv().unwrap()) }
}
scoped_timer.Dispose()
let global = channel(1)
let global_timer = start(|value| { global.send(value) })
async fn receive() -> int { return global.recv().unwrap() }
print(await receive())
global_timer.Dispose()
let failed = channel(1)
let failed_timer = start(|_value| { panic("background callback failed") })
let result = try {
scope { spawn { failed.recv().unwrap() } }
"unexpected success"
} catch error { "timer caught" }
print(result)
failed_timer.Dispose()
// Disposed CLR resources retain their delegates for GC, but cannot wake a
// parked task. This must diagnose a deadlock and leave the scheduler usable.
let disposed_timer = start(|_value| {})
disposed_timer.Dispose()
let abandoned = channel(1)
let disposed_result = try {
scope { spawn { abandoned.recv().unwrap() } }
"unexpected success"
} catch error { "disposed timer" }
print(disposed_result)
async fn recover() -> int { sleep 1ms; return 42 }
print(await recover())
Exporte um tipo .NET por um arquivo Zolo e importe-o com um alias em outro. Construtores, factories, propriedades e overloads continuam tipados.
// expected:
// zero
// i32
// i32
// int
// text:Zolo
// static:i32
// static:int
// str-Any
// str-Any
// text:again
// text:factory
// i32
use extern_fixtures::api::{self as tools, Pick as Counter, make}
let zero = Counter()
print(zero.Kind)
let narrow_value: i32 = 1
let narrow = Counter(narrow_value)
print(narrow.Kind)
print(narrow.Get(narrow_value))
print(narrow.Get(1))
print(narrow.Get("Zolo"))
print(Counter.Static(value: narrow_value))
print(Counter.Static(1))
let dynamic: Any = 7
print(narrow.Named(y: dynamic, x: "Zolo"))
print(narrow.Named("Zolo", dynamic))
print(narrow.Self().Get("again"))
let created = make()
print(created.Get("factory"))
let qualified = tools.Pick(narrow_value)
print(qualified.Get(narrow_value))
O editor analisa o corpo C# com Roslyn, mostrando erros no arquivo original,
completion e hover. O plugin requer .NET 8 ou superior. Projetos precisam do
SDK no build; dependências NuGet devem ter packages.lock.json. VM, native e
LLVM suportam esse provider. Um build WebAssembly sem host .NET informa que o
provider não está disponível.
Veja também