Skip to content

Script Native Types

Liu.Yandong.Hanks edited this page Sep 12, 2026 · 3 revisions

脚本原生类型

用源生成器把 C# Core 方法暴露为脚本 Type,并在类型已证明时由编译器直调。

适用于 4.0.0。

首页 · 宿主集成 · .NET 宿主 API 参考

Important

这与脚本 native func 以及 NativePackages(fs / http)不是同一概念。

  • 脚本原生类型:C# 上的 [NativeType] / [Export],生成 Datum 适配器、脚本 Type 与编译器目录。引擎自有的 String / Number / Int64 / UInt64 也走这条路径。
  • native func:脚本模块函数的显式 ABI。
  • 扩展包:宿主通过 NativePackages 启用的 fs、http。导入后是脚本模块,但类别是扩展包,不是「原生模块」。
本页目录

何时使用

当宿主全局应当是带类型化 Core 方法的脚本 Type,而不是 BondingFunction 或普通 CLR 委托时使用本路径。

应用 Type 必须通过 WithNativeTypes、AddNativeType<T>() 或 AddNativeTypes 加入引擎 catalog;ClearNativeTypes() 会清掉已选的应用 Type。引擎把它们注册到共享全局。每个类型必须是带 [NativeType] 与生成 Register 的 sealed 顶层 ScriptObject。包类型请用 WithPackages,不要列入 NativeTypes。

声明 Type

引用 AuroraScript.JIT 即会自动带上宿主源生成器(打在包内 analyzers/dotnet/cs/,没有独立的 Generators NuGet 包)。解决方案内引用 AuroraScript 工程时同样会挂上分析器。

声明 public sealed partial 顶层类,并标注 [NativeType]。Persistence / OnlyRun 把直调发到 AuroraScript.Generated 程序集,因此不支持 internal NativeType。仅含静态成员、且没有导出构造函数的 Type 可以派生 ScriptObject,也可以不派生;含实例导出或导出构造函数时必须派生 ScriptObject。生成代码会加上 IAuroraNativeInstance 标记。

using AuroraScript.Hosting;
using AuroraScript.Runtime;
using AuroraScript.Runtime.Types;

[NativeType("Stats")]
public sealed partial class StatsSupport : ScriptObject
{
    [Export("mean", MatchFailure.ReturnNaN)]
    public static double MeanCore(double a, double b) => (a + b) / 2D;

    [Export("echo", MatchFailure.Throw)]
    public static ScriptDatum EchoCore(ScriptDatum value) => value;
}

通常一次在引擎上选出应用 Type。引擎把它们注册到共享全局:

var options = EngineOptions.Default.WithCompiler(compiler =>
    compiler.AddNativeType<StatsSupport>().AddNativeType<Vec2>());

需要挂到嵌套对象、或不在 catalog 中的 Type 时,再调用生成的 Register:

StatsSupport.Register(global);

Register 接受任意 ScriptObject。不要手写 [AuroraGeneratedExport] / [AuroraGeneratedConstant] / [AuroraGeneratedNativeObject]。

Export 的归属直接由 CLR 形状决定:static Core 属于脚本 Type 对象,instance Core 属于普通 NativeType 实例。引擎自有 NativeReceiverType 的实例 Core 使用独立的 [ReceiverExport],因此不再需要额外的 Target 枚举。

可实例化的原生 Type

同一 [NativeType] 也支持固定形状的原生实例。含实例导出的类型必须派生 ScriptObject。原生字段和 getter/setter 保持 CLR 存储或 Core 实现;未标注 [Export] 的 CLR 成员不会经反射回退暴露。普通实例方法位于该 NativeType 独享的生成原型上。

using AuroraScript.Hosting;
using AuroraScript.Runtime.Types;

[NativeType("Vec2")]
public sealed partial class Vec2 : ScriptObject
{
    [Export("x")] public double X;
    [Export("y")] public double Y;

    [Export]
    public Vec2(double x, double y) : base(NativePrototype)
    {
        X = x;
        Y = y;
    }

    [Export("length")]
    public double LengthCore() => Math.Sqrt((X * X) + (Y * Y));
}

脚本即可构造并使用实例:

var vec = new Vec2(3, 4);
return vec.length();

宿主也可以 new Vec2(3, 4) 并以 ScriptDatum.FromObject(vec) 传入脚本。

规则:

  • 实例字段必须是 public 的 double、int、bool 或 string。
  • 实例方法使用与静态导出相同的 Core 签名,但没有 thisObject 参数。
  • 只要存在普通实例方法,生成器就会创建惰性初始化的 NativePrototype,并通过 RegisterNativeMembers 按 Writable、Enumerable 注册原型成员;没有可写成员时才冻结。所有用户编写的构造函数都必须显式调用 base(NativePrototype);生成器自行补充的构造函数会自动调用。
  • 仅含原生字段、getter/setter 或静态导出的类型不会生成 NativePrototype,此时沿用普通 ScriptObject 构造函数,不要写 base(NativePrototype)。
  • 原生字段以及 IsGetter/IsSetter 导出由生成的 GetPropertyDatum / SetPropertyDatum 重写直接路由,不进入 HiddenClass,也不注册为原型方法。
  • 原型方法不可枚举,因此动态调用 vec.length() 有效,但 Object.keys(vec) 只包含可枚举的原生字段和动态自有属性。
  • 脚本 new 需要恰好一个标了 [Export] 的 public 构造函数。没有导出构造函数时,该 Type 不可构造。
  • 导出的静态方法仍写在实例 CLR 类上,可以访问其私有字段。
  • 不要把原生字段放进 HiddenClass 槽位;脚本额外添加的属性仍走普通对象路径。

编译期证明局部变量始终持有某一原生对象类型后,该局部以 CLR 类型存储(例如 Vec2),而不是 ScriptDatum。new、字段读写、++/--、+= 一类复合赋值以及方法调用直接绑定 CLR 构造函数、字段和方法(newobj / ldfld / stfld / callvirt),不再装箱拆箱。被闭包捕获的局部、被赋成无法证明类型的值,以及无法证明的接收者(例如函数参数)仍走动态属性协议。

静态 [Export] 方法与 public static readonly double 常量挂在生成的 Type 上(例如 Vec2.Type)。返回 Vec2 的静态工厂会保留具体原生类型证明。

脚本用 context player as Vec2; 把当前 ScriptContext.UserState 证明为该实例类型。每个使用该名称的函数入口加载一次并缓存。export native func current() Vec2 的 $native 返回 CLR Vec2;已证明的调用点不再经过 ScriptDatum.FromObject / ToObject。类型必须是 public 并列入 WithNativeTypes。

直接导出 CLR 属性

已有 public 实例属性时,将 [Export] 放到 get 访问器上即可,无需再写转发用的 Core 方法:

public int Count
{
    [Export("count", IsGetter = true)]
    get => _count;
}

目前不支持把 [Export] 放在属性声明上。getter 必须无参数、无 context 前缀,返回类型属于生成器支持的类型。显式填写脚本成员名;生成器将 CLR get_Count 记录为入口,类型已证明的读取直接调用它,动态读取沿用生成的适配器。IsSetter 仍用于一个必需参数且返回 void 的实例方法。

内置 Date 的 Year、Month、Day、Hour、Minute、Second、Millisecond、DayOfYear 均采用此方式,已移除对应 *Core() 包装器。Date 保留已有动态适配器及冻结原型上的 getter。DayOfWeekCore、TicksCore 仍负责枚举/整数到脚本 Number 的转换,不能直接用不同返回类型的 CLR 属性替换。

减少包装器的明确收益是减少重复实现;原来的简单方法可能被 JIT 内联,因此不承诺固定的运行性能提升。

NativeType 与 TDoc

实现 INativeTypedDocument,并把类型列入 WithNativeTypes 后,脚本 tdoc Vec2 { x 3, y 4 } / tdoc Vec2 [3, 4] / tdoc Flag false 以及宿主 TDoc.parse / TDoc.stringify 会直接构造该 NativeType,而不是 ClrInstanceObject。

WriteTypedDocument 决定规范形态:WriteMember 写成对象,WriteElement 写成数组,WriteValue 写成标量(null / boolean / number / string)。ReadTypedDocument 通过 TypedDocumentInput 读取这三种形态。构造由 NativeType 生成器补上,不必手写 CreateTypedDocument()。

详见 宿主集成 与 Typed Document。

Core 签名

支持的参数与返回类型:

  • double、int、long、ulong、bool、string
  • CLR int 对应脚本小写 int32。检查边界要求精确有符号 32 位整数;已证明的直调使用 System.Int32 ABI,整数算术按 CLR int 环绕。
  • CLR long / ulong 对应脚本 int64 / uint64。同类型脚本算术按 CLR 整数环绕;混合运算成为 double。
  • CLR void 返回:过程。直调语句不物化结果;动态、导出或取值调用观察到脚本 null,与脚本 native func work(...) void 一致。
  • ScriptDatum
  • 任意 ScriptObject 子类(ScriptArray、packed array、Path、Proxy、Regex、Date、HashMap、Error、ClosureFunction、包装器)

可选隐式前缀,且只能按此顺序:

  1. ScriptContext ctx — 不是脚本实参。
  2. ScriptObject thisObject — 接收者;参数名必须为 thisObject。

允许尾部 C# 默认值。params double[] 与 params ScriptDatum[] 仅用于 Datum 适配器。普通对象 NativeType 也可以用显式 DynamicAdapter 保留宽泛的动态行为,同时导出共享该适配器的固定参数 Core 重载供编译器直调;Path.append 的一段、两段和三段重载采用这一模式。

public static readonly double 字段可标 [Export("PI")],成为脚本常量。未遮蔽读取发射 ldsfld,而不是装箱的 ldc.r8。

[AuroraParam(MatchLevel.Exact)] 或 Strict 收紧适配器强制转换。MatchFailure 控制不匹配行为:Default 对数值返回推断为 NaN,否则为 null;Throw 抛出 AuroraRuntimeException。

对象返回值通过 ScriptDatum.WriteObject / FromObject 保留原来的 ValueKind,因此 Array、Date、Error 不会塌成普通 object。

不支持:async、Span/ref/out/in、泛型方法、嵌套类型、record、空导出名、params 与 [AuroraParam] 同时使用、把 ctx/thisObject 放在脚本参数之后。

直调与适配器

参数类型已证明时,编译器直接调用 Core 方法。条件:

  • 未遮蔽的全局接收者(Math.abs(x),而不是 var m = Math; m.abs(x))。
  • 无 params 的 public Core 方法。
  • 实参类型可证明兼容,含尾部可选默认值。
  • 目录元数据存在。引擎程序集始终扫描。应用 Type 必须列入 CompilerOptions.NativeTypes:
var options = EngineOptions.Default.WithCompiler(compiler =>
    compiler.AddNativeType<StatsSupport>().AddNativeType<Vec2>());

params 导出、spread 实参、被遮蔽的全局以及未证明的实参类型走生成的 Datum 适配器。@global() 中成员块形式的 declare type 不会驱动编译器推断(方法类型 declare type Name(...) ReturnType; 例外,它是编译期契约)。

引擎内置原始接收者

引擎用同一套 NativeType 生成器与编译器目录实现不可变 String / Number / Int64 / UInt64 成员,而不是手写原型回调。

NativeType.NativeReceiverType 声明引擎自有的 string、double、long、ulong 存储,不是宿主扩展原始接收者表示的入口。ReceiverExport 把 static Core 标记为脚本实例成员:可选 ScriptContext 之后的第一个参数是原始 CLR 接收者。Export 标记的 static Core 始终属于 Type 对象。

NativeType.NativeConstructor 命名一个已导出的静态 Type Core,其返回类型必须匹配 NativeReceiverType;String(...) 与 new String(...) 共用它。StringValue.Register(Global) 取代手写 StringConstructor。

[NativeType(
    "String",
    NativeReceiverType = typeof(string),
    NativeConstructor = nameof(CreateCore))]
public sealed partial class StringValue
{
    [ReceiverExport("trim")]
    public static string TrimCore(string value) => value.Trim();

    [Export("valueOf")]
    public static string CreateCore(string value = "") => value;
}

省略 DynamicAdapter 时,生成器按常规强制转换、失败处理与结果写入生成动态入口。指定 DynamicAdapter 后直接使用已有回调,不再生成动态包装器;等元数重载必须共享同一个显式适配器。零参 getter 设 IsGetter = true;匹配脚本名称的一参 void Core 可设 IsSetter = true。普通对象 NativeType 的 getter/setter 走生成的属性重写;NativeReceiverType 不支持 setter。

方法与静态常量的 Writable、Enumerable 默认均为 false。Native 实例字段默认按 CLR 可变性决定是否可写,并默认可枚举;作者可显式覆盖。可写的方法或常量槽可能被脚本替换,不进入 Native 直调目录。只要生成的原型或 Type 含可写槽,它就不会被冻结。

编译器在类型分析阶段选定并保存已证明的签名,再发出直调。String 的 substring / slice 只导出 int 索引,第二个索引仍是 end 而不是 length。已证明的 string 接收者走 CLR 直调;未知接收者与不支持的调用形状保持动态分派。split 返回 ScriptArray;match / matchAll 返回 ScriptDatum,因为结果形状不是无条件的。

String.fromCharCode 取原生 int(第一个 UTF-16 代码单元);String.compare 比较两个 string 的首个代码单元,任一侧为空时返回 1。填充宽度是原生 int,填充字符仍取第一个 UTF-16 代码单元。

Number 的 double 接收者允许 int / uint Core 特化,必须带显式兼容适配器,不能把任意动态 Number 静默收窄为 Int32。NumberValue.FormatString 对 double / int / uint / long / ulong 重载;精确 64 位值不经 double。Int64 / UInt64 有冻结自有原型:toString(16) 格式化全部 64 位大写十六进制;无参为不变区域性十进制;其它进制仍选十进制。Number 保留当前区域性格式化以及历史 Int32 十六进制(含大于 Int32.MaxValue 的 UInt32 兼容路径)。

脚本 declare type

在 Resolver 可见的 @global() 文件中手写编辑器契约。不要使用 declare class、declare module 或结构体 type 来描述宿主 Type。成员签名仅供语言服务,不参与类型推断。

@global();

declare type Stats {
    static func mean(Number a, Number b) Number;
}

declare type Vec2 {
    constructor(Number x, Number y);
    Number x;
    func length() Number;
}

仅含 static 成员、没有 constructor 的 Type 不可 new。typeof(Math) / typeof(console) 为 "type"。

诊断

代码 级别 含义
AURORAEXP001 Error 无效的 NativeType 声明
AURORAEXP002 Error 不支持的导出成员或签名
AURORAEXP003 Error 重复的脚本成员名

引擎内置

引擎始终注册 Math、JSON、TDoc、console、Conv8 和 HotPatch 为基础设施 Type,不必列入 WithNativeTypes。typeof 为 "type";没有导出构造函数时 new Math() / new console() / new Conv8() 失败。

Math.max / Math.min 使用 params double[],因此始终走适配器;Math.abs 与 Math.PI 在未遮蔽且类型已证明时可直调或 ldsfld。Conv8 的固定参数读写在 UInt8Array 与整数偏移已证明时可直调 Core。

console.log 复用普通导出重载选择:原始类型实参可选择对应 CLR 重载,一个或两个未知类型实参可直调 ScriptDatum 重载。spread 或不支持的参数形状仍使用共享动态适配器。

后续步骤

AuroraScript.JIT 4.0.0

入门

语言

脚本 API

宿主集成

工具

项目

Clone this wiki locally