☰
BAML for C 托管契约探针(ManagedContractProbe):公共类型与翻译证据的编译期验证指南
2026/9/25 2:20:08 网站建设 项目流程
  • 编程语言
  • AI Agent
  • 编译器
  • CLI
  • 人工智能

【免费下载链接】baml

The programming language for agents

项目地址:https://gitcode.com/gh_mirrors/ba/baml
点击查看免费下载

导读

Baml.Bridge.ManagedContractProbe是 BAML C# 桥接层(baml-bridge,.NET 10 运行时)内部的一组"编译期设计证据"探针,用于在产物发布之前,把 gate B5、B6、B10 尚未定案的托管契约证据(optionality 与 nullability 的正交性、语义部分状态、不可变请求/客户端/媒体、SafeHandle 所有权、全部动态值种类与描述符身份、规范泛型绑定、生成代码注册、拥有型集合与字节、精确数值边界、显式不支持的 CLR 形状、环检测、冻结图限制)编译并逐项验证。读完本文,你将掌握该探针的验证矩阵、每个主题域的底层类型设计、关键常量与 ABI 数值,以及如何在仓库中定位与复跑这些证据。

探针的定位:编译期证据,而非最终运行时包

ManagedContractProbe/README.md 开宗明义:这是一个repository-only(仅存在于仓库内)的 .NET 10 可执行程序,它"编译"(compile)的是 gate B5、B6、B10 的未解决(unresolved)托管契约证据。它与 baml-bridge 运行时说明 描述的正式运行时、NuGet 包是两层东西:

  • 运行时:baml-bridge是随 BAML CLI 生成的 C# 客户端共同发布的 .NET 10 托管运行时 + 原生资产,面向osx-arm64、osx-x64、linux-arm64、linux-musl-arm64、linux-x64、linux-musl-x64、win-x64、win-arm64等 RID,支持普通 JIT 与裁剪(trimmed)部署,但不支持 NativeAOT(以BAML0019失败构建)。
  • 探针:ManagedContractProbe及其同类 probe 是仓库内专用工程,不对外发布,职责是在设计阶段把尚存争议的托管契约"钉死"成可编译、可断言、可审计的证据。

因此,README 特别声明:"These definitions are compiled design evidence, not the final runtime package."(这些定义是编译后的设计证据,不是最终运行时包)。相应地,arity 3–32 的联合类型、生成的 V1 接缝(seam)、异常/取消身份、原生媒体还原、流生命周期、程序与原生引导等既有结论仍由仓库中其他探针保持权威,本探针不越俎代庖。

工程配置本身也体现了"证据"的苛刻性——Baml.Bridge.ManagedContractProbe.csproj 设置了net10.0+LangVersion 14.0、Nullable enable、TreatWarningsAsErrors、Deterministic,并且开启EnableTrimAnalyzer与IsTrimmable,让任何警告、非确定性输出或裁剪分析问题都会直接失败编译。

验证入口与检查清单:Main 输出的契约摘要

探针入口 Program.cs 依次执行七组验证,最终输出一组key=value的契约摘要:

输出键断言值语义
optional_nullableorthogonal_completeoptionality 与 nullability 正交且完整
stream_statepending_incomplete_complete流状态三态(Pending/Incomplete/Complete)
media_values4x_url_bytes_base64_file_owned四类媒体的四种来源路径全部"拥有"数据
http_requestimmutable_duplicate_headers_fresh_messages请求不可变、保留重复头、每次转换生成新消息
client_retryimmutable_structural_checked客户端/重试策略不可变且结构相等
handlesafe_clone_lease_dispose_identitySafeHandle 克隆/租约/释放身份正确
baml_valuekinds_14_structural_typed14 种动态值种类,结构相等且类型化
descriptor_kindsunknown_plus_14_value_shapes描述符含 Unknown 加 14 种值形状
descriptorsalias_literal_nominal_generic_union别名/字面量/名义/泛型/联合描述符
dynamic_inspectionenum_class_union_public动态值的 enum/class/union 可公开检查
dynamic_nullexplicit_nullable_onlyBAML null 只允许显式可空目标
collectionsowned_readonly_canonical_maps拥有型只读集合与规范 map
generic_bindercanonical_and_fail_closed泛型绑定只接受规范闭包、失败关闭
limitsdepth_collection_bytes_nodes_bigint_cycle深度/集合/字节/节点/bigint/环限制
partial_projectionsemantic_states语义部分投影
unsupported_clrexplicit不支持的 CLR 形状显式拒绝
public_contractaudited公共契约经反射审计

每组验证都有独立方法(VerifyOptionalNullableAndPartialState、VerifyMediaAsync、VerifyRequestClientAndHandleAsync、VerifyDynamicValues、VerifyGenericBinder、VerifyLimitsAndCycles、VerifyPublicShapeInvariants),并依赖两个自建断言助手Require(condition, message)与Expect<TException>(Action)(Program.cs#L1138-L1160),任何违反都抛出带明确消息的InvalidOperationException。

可选性与可空性:正交的三种状态模型

README 点名的第一个主题是"optionality versus nullability"(可选性与可空性)。探针以 OptionalNullableState.cs 中的两个只读结构体承载这个正交模型:

  • BamlOptional<T>:表达"调用方是否提供了参数"。核心是IsSet、Unset、FromValue(value)、TryGetValue(out T),并提供从T的隐式转换。未设置时访问Value抛InvalidOperationException。
  • BamlNullable<T>:表达"值本身是否为空"。核心是IsNull、Null、FromValue(value)、TryGetValue(out T)与模式匹配用的Match(onNull, onValue)(两个委托均拒绝 null 参数)。关键语义:BamlNullable<T>.FromValue(null!)会坍缩为 null 态(构造时hasValue = value is not null),这与BamlOptional<T>.FromValue(null)截然不同——后者保持IsSet = true且值为 null。

VerifyOptionalNullableAndPartialState 逐条钉死了不变量:

  1. 显式默认值不坍缩:BamlOptional<long>.FromValue(0)必须IsSet == true且Value == 0,绝不因为"等于默认值"而变成 unset;
  2. 显式 null 不坍缩:BamlOptional<string?>.FromValue(null)必须IsSet == true且值就是 null,且!= default;
  3. 组合保留全部状态:BamlOptional<BamlNullable<long>>的default(未提供)、BamlNullable.Null<long>()(提供了但为空)、BamlNullable.FromValue(42L)(提供了且有值)三者必须能区分——这正是"正交"的含义;
  4. 流状态三态:BamlStreamState<T>的Pending(尚未产生部分值)、Incomplete(携带部分值,如"par")、Complete(最终值,可以是 null)必须各自成立,且Complete(null) == Complete(null)。

配套的语义部分投影证据在 Program.cs#L190-L206:ResumePartial类型的RequiredWhenReady、DoneField为可空字段、NonNullPartial为必填子对象、WithState携带BamlStreamState<string?>——验证"部分投影"不会把未就绪字段错误地当成缺失标记。

这一设计与 src/README.md 的运行时约定完全一致:普通可空值位置在无歧义时直接用 C# 可空类型;泛型可空引用绑定因为 CLR 无法区分typeof(string)与typeof(string?),必须使用BamlNullable<T>。

不可变请求、客户端与媒体:快照与脱敏

HTTP 请求

BamlHttpRequest在 VerifyRequestClientAndHandleAsync 中验证:构造时(请求 ID、方法、URL、头列表、content-type、body 字节)即做拥有型快照——调用方随后headers.Clear()、修改body[0]均不影响快照;ToHttpRequestMessage()每次返回全新的HttpRequestMessage与Content(!ReferenceEquals(first, second)),且重复头(X-Trace: one、X-Trace: two)被完整保留;对第一个消息Dispose后再读第二个消息的 body,结果不受影响。ToString()则脱敏 URL 中的secret与 body 内容。

客户端与重试策略

BamlRetryPolicy(maxRetries, initialDelayMilliseconds, maxDelayMilliseconds, multiplier)与BamlClient(name, type, subClients, retry, counter)均不可变:探针把构造时传入的sourceChildren数组在事后篡改为"mutated",client.SubClients[0].Name仍须是"child";结构相等要求client.Equals(equivalent)且GetHashCode()一致。负的重试次数、非法BamlClientType值都会抛ArgumentOutOfRangeException。BamlClient.FromShorthand("name")得到BamlClientType.Primitive。

四类媒体

Media.cs 定义了共享的MediaPayload:FromUrl存 URL、FromBytes/FromBase64/FromFileAsync都复制字节为快照(bytes?.ToArray()),FromFileAsync在删除源文件后仍能取回完整内容("eagerly owned")。BamlImage/BamlAudio/BamlVideo/BamlPdf四类 sealed 值类型均提供FromUrl、FromBytes、FromBase64、FromFileAsync与TryGetUrl/TryGetBytes。

VerifyMediaAsync 验证:

  • 字节复制:传入可变数组后篡改首字节,取回的快照首字节仍是原始值;
  • URL 脱敏:带?token=secret#fragment的 URL 在结构化值中保留完整 URL(videoUrl.Contains("token=secret")),但ToString()不得泄露token=secret或fragment;
  • 文件急切拥有:BamlPdf.FromFileAsync读入后删除文件,TryGetBytes仍返回完整 PDF 头%PDF;
  • 结构相等/哈希:bytes 与 base64 同内容相等,URL 与 bytes 形式不相等,GetHashCode一致。

SafeHandle 所有权:克隆、租约、释放

README 点名 "SafeHandle ownership"。探针在 Program.cs#L386-L432 中通过BamlHandle与原生引用计数表NativeReferenceTable.Releases验证:

  • Clone()产生独立所有者(!ReferenceEquals(original, clone)),但两个包装器指向同一原生资源(租约返回相同 identity);
  • Dispose()幂等且独立:original.Dispose()两次后original.IsClosed,但clone仍可租约;对已关闭句柄Clone()抛ObjectDisposedException;
  • 释放恰好一次:两个包装器各自释放后,原生表Releases精确 +2;
  • 并发安全:64 个并发租约任务与 1 个Dispose竞态,最终必须正常关闭且不产生悬挂访问。

这也解释了 VerifyPublicShapeInvariants 的反射断言:媒体类型与BamlHttpRequest不实现IDisposable(无原生句柄),而BamlHandle必须实现;BamlHandle的任何公共属性类型都不得是IntPtr或SafeHandle(原始原生键绝不暴露给应用),BamlValue的任何公共属性也不得是裸object。

动态值:14 种种类与描述符身份

DynamicValues.cs 定义了动态值载体与两种枚举:

枚举数值取值
BamlValueKind : int0–13Null、Bool、Int、Float、BigInt、String、Bytes、List、Map、Enum、Class、Union、Media、Handle
BamlTypeDescriptorKind : int0–14Unknown + 上述 14 种形状(一一对应、偏移 +1)

BamlTypeDescriptor(公开只读属性恰好为Kind、Fqn、Arguments、IsNullable、Alias、Literal)有严格形状校验(DynamicValues.cs#L151-L182):Enum/Class/Handle必须携带 BAML FQN;List恰好 1 个类型参数、Map恰好 2 个、Union至少 2 个、其余必须为空;构造参数数组被复制为ReadOnlyCollection,任何 null 参数都拒绝。

VerifyDynamicValues 验证 14 种BamlValue工厂(Null、Bool、Int、Float、BigInt、String、Bytes、List、Map、Enum、Class、Union、Media、Handle)与Enum.GetValues<BamlValueKind>()完全一一对应且顺序一致;再验证:

  • 描述符身份:BamlValue.Alias("probe.UserId", ...)的Type.Alias必须保留,普通BamlValue.String("fixed")的Alias/Literal为 null;Literal元数据与载荷矛盾(如Int(1)配"01")抛异常;
  • 上下文无关解码不猜测:带 alias/literal 的值TryGet<string>()必须失败(不能脱离出现点猜类型),Union 的激活臂元数据与载荷矛盾必须抛异常;
  • null 的纪律:BamlValue.Null只允许解码到BamlValue、long?、BamlNullable<string>;解码到object、string、Person、接口/具体集合一律拒绝;BamlValue.From<object?>(null)、From<string?>(null)、From<List<long>?>(null)一律抛BamlTypeMappingException;
  • 隐式 null:long? input = null经BamlValue.From必须得到BamlValue.Null;
  • 同构容器不猜类型:空列表/异构列表的类型参数为Unknown,异构列表TryGet<IReadOnlyList<long>>必须失败;
  • 公共检查方法:TryGetEnumVariant、TryGetClassFields(返回拥有 wire 顺序的只读快照)、TryGetUnion只能对正确种类返回 true,否则返回 false 且 out 参数为 null。

规范泛型绑定:白名单闭包与显式拒绝

Union2AndBinder.cs 中的BamlClrTypeBinder.Describe(type, path)实现了"canonical generic binding":

支持的闭包:bool、long、double、BigInteger、string、ReadOnlyMemory<byte>、BamlValue(→Unknown)、四类媒体(→Media)、BamlHandle(→Handle)、已注册类型,以及Nullable<T>/BamlNullable<T>(→ 内层描述符 +IsNullable=true)、IReadOnlyList<T>(→List)、IReadOnlyDictionary<K,V>(→Map,键必须是string、生成枚举或字面量,否则拒绝)。

显式拒绝的 CLR 形状(VerifyGenericBinder 中的完整列表):short、int、uint、ulong、float、decimal、List<long>、Dictionary<string,long>、long[]、object、JsonElement、JsonNode、JsonDocument、DateTime、DateTimeOffset、DateOnly、Guid、Uri、ValueTuple<long,string>、BamlOptional<long>、BamlUnion<string,long>、BamlNullable<BamlNullable<string>>(冗余嵌套会坍缩 BAML null 状态)、IReadOnlyDictionary<long,string>(非规范键)。每一个失败都必须是BamlTypeMappingException且Path以$T开头。

非规范数值的诊断替代:int的CanonicalReplacement == "long"、float的CanonicalReplacement == "double"——诊断会明确告诉你"该用什么规范类型",而不是笼统报错。

生成代码注册:类型描述符与动态编解码器

RegisterGeneratedTypes 演示了生成代码侧的注册模式:

  • BamlClrTypeBinder.Register(typeof(Person), personDescriptor):把 CLR 类型绑定到 BAML 描述符(probe.Person、probe.Color、泛型probe.Box),重复注册抛InvalidOperationException;
  • BamlDynamicRegistry.Register<Person>(encode, decode):注册名义类型的双向编解码器;解码端要求载荷描述符匹配、可通过TryGetClassFields公开检查,再映射回Person { Name, Age };
  • BamlDynamicRegistry.RegisterCanonicalList<long>()与RegisterCanonicalStringMap<long>():注册规范集合编解码,解码产物为ReadOnlyCollection<T>/ReadOnlyDictionary<string,T>(DynamicValues.cs#L840-L978)。

注册之后,BamlValue.From(person)/classValue.As<Person>()、BamlValue.From(listInput)/encodedList.As<IReadOnlyList<long>>()便可在反射之外工作——这正是 src/README.md 强调的"生成代码的字段编解码器与工厂从不通过反射发现模型成员、可裁剪安全"的实现证据。

拥有型集合与精确数值边界

BamlValue.List与BamlValue.Map构造即复制快照:List 用ReadOnlyCollection<BamlValue>,Map 按键StringComparer.Ordinal排序、拒绝重复键与 null 值/键(DynamicValues.cs#L270-L355)。探针验证解码结果也是只读的:对decodedList执行((IList<long>)...)[0] = 0抛NotSupportedException,对decodedMap执行Add同样抛异常(Program.cs#L581-L616)。

数值边界方面:BAMLint映射到long且必须落在 BAML 范围[-2^62, 2^62-1](BamlInteger校验,探针用BamlInteger.Max构造BamlValue.Int);bigint的十六进制编码长度受BamlBigIntCodec.MaxHexLength = (1 << 28) / 4 + 2约束,超限抛BamlTypeMappingException(DynamicValues.cs#L823-L838)。

冻结图限制与环检测

README 点名"all frozen managed graph limits"(全部冻结的托管图限制)。BamlValueLimits 定义了四组硬限制,任何构造或图遍历都会校验:

限制值触发路径
MaxDepth64嵌套深度超过即抛
MaxCollectionItems1_000_000单集合元素数超限即抛
MaxBytes64 × 1024 × 1024(64 MiB)字节/媒体负载超限即抛
MaxNodes2_000_000图总节点数超限即抛

VerifyLimitsAndCycles 用两个特制测试桩验证"限制必须在访问前拒绝":CountOnlyValues(只报告Count、不真正枚举)触发MaxCollectionItems + 1拒绝;OversizeMemory(MemoryManager<byte>子类,GetSpan直接抛)触发MaxBytes + 1拒绝——证明大小校验发生在读取内容之前。环检测由GeneratedCodecTraversal(Union2AndBinder.cs#L356-L387)用引用相等集合完成:Node.Next = node的自环在遍历到$.next时抛出,且异常Path精确为"$.next"、消息包含 "cycle"。

公共表面审计:反射钉死的 ABI

VerifyPublicShapeInvariants(Program.cs#L874-L1118)用反射把公共契约审计到字节级:

  • BamlTypeDescriptor公开实例属性必须恰好是{Alias, Arguments, Fqn, IsNullable, Kind, Literal},且没有公开构造函数(只能由内部工厂/生成代码构造);
  • BamlValue公开属性恰好{Kind, Null, Type},公开方法恰好{As, BigInt, Bool, Bytes, Equals, Float, From, GetHashCode, Int, List, Map, String, ToString, TryGet, TryGetClassFields, TryGetEnumVariant, TryGetUnion}(Equals重载计一次),同样无公开构造函数;
  • 桥接层自有枚举的 ABI 数值被冻结:BamlValueKind底层int且取值为 0–13;BamlTypeDescriptorKind底层int且为 0–14;BamlStreamStateKind底层int且为 0/1/2;BamlClientType底层long且为 1/2/3——任何偏移都会使反射断言失败;
  • 密封性:BamlImage、BamlValue、BamlTypeDescriptor、BamlClient、BamlHttpRequest必须 sealed;
  • 描述符泛型参数快照:构造Box描述符后篡改源mutableArguments数组,描述符的Arguments[0].Kind仍须是Int。

Union 的二分证据与既有探针的分工

Union2AndBinder.cs#L5-L141 只实现BamlUnion<T0, T1>(arity 2),并验证:

  • 显式工厂FromT0/FromT1,激活分支用内部caseIndex(1/2)区分,default(BamlUnion<T0,T1>)(caseIndex 0)不合法:IsT0/IsT1均为 false,访问AsT0抛"uninitialized";
  • Match/Switch二分支函数式访问;访问未激活分支抛InvalidOperationException;
  • 内部编解码视图ActiveCaseForCodec(1→0、2→1)与ValueForCodec仅限生成代码使用,公开面不暴露;
  • 公开属性恰好{AsT0, AsT1, IsT0, IsT1},且没有CaseIndex/IsValid这类会泄露"未初始化"合法性的属性。

这正是 README 分工声明的落点:BamlUnion的 arity 2 形状由本探针钉死,而arity 3–32 的联合、生成的 V1 接缝、异常/取消身份、原生媒体还原、流生命周期、程序/原生引导仍由仓库中既有的其他探针保持权威(参见 bridge_csharp 测试目录 下的EnumDiscriminantProbe、FailureCancellationProbe、GeneratedCodeContractProbe、StreamMediaAbiProbe、ProgramBootstrapProbe、NuGetPackageSmoke等兄弟工程)。

如何查看与复跑证据

探针是仓库内普通 .NET 工程,可以像查看任何源码一样阅读 Program.cs 与同目录的 DynamicValues.cs、Media.cs、OptionalNullableState.cs、Union2AndBinder.cs。若本机具备 .NET 10 SDK 与 C# 14 编译器,也可以在仓库内通过dotnet run(工程已启用Deterministic与TreatWarningsAsErrors,任何警告都会被当作错误)执行该探针;它不依赖任何外部 NuGet 包或原生二进制,纯托管、自包含,全部断言通过后返回退出码 0 并打印上文那张契约摘要表。

需要提醒的是:该探针是设计证据而非公开发行的 SDK 组件,运行时契约的正式落地仍以 baml-bridge 运行时说明 与 BAML CLI 生成的baml_sdk/为准——探针的作用,是让这些契约在进入最终产物之前,先以最严格、最可复现的方式被"编译并钉死"。

  • 编程语言
  • AI Agent
  • 编译器
  • CLI
  • 人工智能

【免费下载链接】baml

The programming language for agents

项目地址:https://gitcode.com/gh_mirrors/ba/baml
点击查看免费下载

相关推荐

上一篇:Vortex OpenCL开发实战:从零开始编写你的第一个GPU加速程序
下一篇:LunarVim 启动器教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询