C# async await 到 JavaScript Promise:从状态机到原生 await

C# async/await → JavaScript Promise:从状态机到原生 await

Roslyn 已经在最终 Compilation 中完成 async 语义绑定;Jazor 将绑定后的 IOperation 直接 lowering 为 JavaScript async/await

一、两套异步模型,一个翻译目标

C# 的异步和 JavaScript 的异步在"语法糖"层面几乎一模一样——async 标记方法、await 暂停等待。但如果你看编译器底层,它们是两套完全不同的机制。

C# 编译器在运行时执行路径上会为 async 方法生成状态机;但 Jazor 的输入不是这份 IL,也不会反向分析 IAsyncStateMachine。RazorVue 和普通 C# 模块都把最终 Roslyn Compilation 的绑定语义交给 SemanticWalker,因此 await 仍以 IAwaitOperation 的形式可见。

JavaScript 没有这层展开。JS 引擎原生理解 async functionawait——它是语言内置的,不是编译器骗术。

所以 Jazor 做的是语义 lowering:把 Roslyn 已绑定的 await 操作直接表达成 JavaScript 的 async functionawait

二、两段检测:我怎么知道这里要生成 async

SemanticWalker 里,需要判断一个函数体是否包含异步语义的入口有两个场景:匿名函数/Lambda 和方法体。

检测逻辑有两段:

第一段:IOperation 级别的检测。 遍历整个函数体的 IOperation 树,找有没有 IAwaitOperation 节点:

private static bool ContainsAwaitOperation(IOperation? operation)
    => operation is not null &&
       (ContainsOperation(operation, static op => op.Kind == OperationKind.Await) ||
        ContainsAwaitSyntax(operation.Syntax));

第二段:Syntax 级别的兜底。 如果 IOperation 树里没找到 Await 节点(某些边角情况下 Roslyn 可能不生成),就回到语法树里再搜一遍 AwaitExpressionSyntax

private static bool ContainsAwaitSyntax(SyntaxNode syntax)
{
    return syntax.DescendantNodes(descendIntoChildren: node =>
        node is not AnonymousFunctionExpressionSyntax
        and not ...
        and not LocalFunctionStatementSyntax)
        .Any(static node => node is AwaitExpressionSyntax);
}

注意 descendIntoChildren 的条件:碰到嵌套函数边界就停止向下搜索。 这是正确的做法——内部嵌套的 async 函数不应该让外层函数也变成 async

两段搜索有一处命中,就把这个函数标记为 async

三、Lambda 和匿名函数:最自然的 async 传递

ContainsAwaitOperation 返回 true 时,匿名函数和 Lambda 的生成逻辑非常简单——在 ESTree 的 ArrowFunctionExpression 上把 async 属性设为 true

// C#:
Func<Task<int>> f = async () => {
    await Task.Delay(1000);
    return 42;
};

// JS:
const f = async () => {
    await new Promise(r => setTimeout(r, 1000));
    return 42;
};

async 属性是一个布尔值,直接传给 Acornima AST 构造函数。不需要额外的包装。

四、方法体:同样是给 FunctionDeclaration 加 async

对于完整的类方法,生成逻辑同样直接——在 FunctionDeclaration 上设置 async: true

// C#:
[ECMAScriptModule]
public static class MyModule
{
    public static async Task<string> FetchAsync(string url)
    {
    await Task.Delay(1);
    return url;
    }
}

// 编译输出:
export async function fetchAsync(url) {
    const response = await httpClient.getAsync(url);
    return await response.readAsStringAsync();
}

这里的关键是,Jazor 不需要处理 IAsyncStateMachine 的任何细节。Roslyn 已经把方法识别为 asyncSemanticWalker 只需要从绑定后的操作和符号得到异步语义,再把 async 标记传递到输出的 AST 节点。运行时状态机不属于 Jazor 的输入,也不会被 Jazor 重新解析。

五、Task<T>Promise<T> 的类型映射

异步机制是语法层面的,但类型系统也有对应的映射。C# 里的 Task<T> 在 Jazor 中映射到 IPromise<T>

// CLR 映射:
// System.Threading.Tasks.Task<T>  →  IPromise<T>
// System.Threading.Tasks.Task     →  IPromise<void>

IPromise<T> 是 Jazor 的 ECMAScript 绑定层定义的一个接口,它直接对应 JavaScript 的 Promise:

[ECMAScript]
public interface IPromise<T>
{
    IPromise<TResult> Then<TResult>(Func<T, TResult> onFulfilled);
    IPromise<TResult> Then<TResult>(Func<T, IPromise<TResult>> onFulfilled);
    IPromise<T> Catch(Func<object, T> onRejected);
    // ...
}

当你在 C# 里写 Task<int> 作为返回类型时,Jazor 会自动生成返回 Promise<number> 的 JavaScript 函数。这个映射对调用方完全透明——C# 代码看起来在操作 Task<T>,编译到 JS 后就是标准的 Promise 链。

六、await 表达式的翻译

VisitAwait 方法处理 IAwaitOperation。翻译逻辑非常直接:

public override Node? VisitAwait(IAwaitOperation operation, SenseArgument argument)
{
    var operand = Translate<Expression>(operation.Operation, argument);
    return new AwaitExpression(operand);
}

没有状态机编号、没有 MoveNext、没有 SetResult。就是简单地把 C# 的 await 变成 JavaScript 的 await

这之所以可行,是因为 Jazor 有两个前提保证:

  1. C# 的 async 方法已经由 Roslyn 完成了语义分析。 Jazor 拿到的是经过绑定和验证的 IOperation 树,await 点的位置和操作数类型都是确定的。

  2. JS 的 await 语义是 C# await 的子集。 C# 的 await 能等待任意 awaitable;Jazor 对已映射的 Task surface 直接生成 JS await,包括 ConfigureAwait 的 Promise lowering。

异步 Lambda 的变量捕获

C# 的 async Lambda 会捕获外部变量(closure)。在 IOperation 里,被捕获的变量表现为 IParameterReferenceOperationIFieldReferenceOperation,Roslyn 已经标记了哪些变量被提升到了闭包。Jazor 在翻译时不需要手动做变量提升——因为 JS 的 closure 语义天然就支持捕获外部变量,不需要像 C# 编译器那样生成 display class。

// C#:
int factor = 2;
Func<Task<int>> f = async () => {
    await Task.Delay(100);
    return factor * 10;   // factor 被捕获
};
// JS:
let factor = 2;
const f = async () => {
    await new Promise(r => setTimeout(r, 100));
    return factor * 10;    // JS closure 直接捕获,不需要 display class
};

这是 Jazor 在异步 lowering 上的另一个"免费午餐"——JS 的闭包模型和 C# 的 lambda 捕获高度兼容,不需要额外翻译。

七、switch 表达式里的异步检测

这是模式匹配 blog 里也提到过的一个交叉点。如果 switch 表达式的某个 arm 里包含了 await,整个 switch 表达式会被包进一个 async IIFE:

// C#:
var result = await (obj switch { ... });

// 如果检测到 ContainsAwaitOperation:
var result = await (async () => { ... })();

检测时机是在把 switch 表达式 Lower 成 IIFE 的时候——如果 ContainsAwaitOperation(operation) 返回 true,IIFE 的箭头函数就标记为 async

八、什么场景会失败:异步的边界

Jazor 目前对异步的支持有明确的边界:

  • 已落在当前 lowering 支持范围内的 async 方法/Lambda:生成 async functionasync () => {};具体方法体仍受普通 C#、CLR 映射和使用点边界约束。
  • Task / Task<T> 返回类型:支持。自动映射到 Promise
  • await 表达式:支持。直接翻译为 JS await
  • ConfigureAwait(bool/options):当前 CLR 映射将其规约到 Promise 语义;它不会在 JS 中保留 .NET 的同步上下文捕获。
  • 未被 CLR 白名单映射的自定义 awaitable:不支持;是否可用由具体映射和使用点裁决。
  • async 迭代器(IAsyncEnumerable<T>:不支持。JS 有 for await...of,但 Jazor 还没打通这条链路。

边界策略跟 Jazor 的整体哲学一致:支持的是"能干净映射"的部分,不支持的就明确报错,不静默回退。

九、小结

C# async/await 到 JavaScript 的翻译是 Jazor 里最"自然"的一块 lowering——不是因为简单,而是因为两个语言的异步模型在语法层面高度相似。

但"相似"背后有一个容易被忽略的事实:Jazor 处理的是 Roslyn 保留下来的源级绑定语义,而不是 C# 编译器生成的状态机 IL。 它只需把 IAwaitOperation 和已映射的异步类型用 JS 的原生语法表达出来。

整个过程的支柱是 ContainsAwaitOperationContainsAwaitSyntax 两个检测方法:它们扫描 IOperation 树和语法树,在函数边界内识别异步特征,然后把 async 标记传递给输出的 AST 节点。不需要重写状态机、不需要模拟 MoveNext——JS 的 await 本身就是我们要的东西。

项目地址:https://github.com/devhxj/Jazor

posted @ 2026-05-11 15:26  形而下晓寒  阅读(18)  评论(0)    收藏  举报