Slang 结构体(struct)语言参考与一致性测试全解析
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
导读
本文以 Slang 语言参考文档 docs/language-reference/types-struct.md 为骨架,完整讲解 Slang 中struct类型的语法、成员种类、构造与初始化、成员函数、属性(property)、下标(subscript)与函数调用运算符、内存布局以及This类型关键字等全部规范内容,并以仓库中 docs/generated/tests/conformance/types-struct/ 目录下的 30+ 个一致性测试用例为佐证,从语法规范到编译器行为逐一验证。读完本文,你将掌握 Slang 结构体的完整语义、正确写法与常见误区,并了解如何通过一致性测试用例验证这些语言行为。
注:本文涉及的所有
.slang测试文件均位于docs/generated/tests/conformance/types-struct/目录,其元信息(//META:注释)标注了对应的doc_ref锚点,可直接追溯到语言参考原文。
结构体声明语法:四种声明形式
Slang 的struct声明由文法规则struct-decl定义,共有四种形式:
1. 无成员体(no-body)声明—— 仅声明结构体类型存在:
struct-decl = [modifier-list] 'struct' [identifier] [generic-params-decl] [':' bases-clause] ['=' simple-type-spec] ';'2. 带成员体(with-members)声明—— 定义结构体的布局与非布局成员:
struct-decl = [modifier-list] 'struct' [identifier] [generic-params-decl] [':' bases-clause] ('where' where-clause)* '{' member-list '}'3. 链接期 extern 类型声明—— 声明定义于其他模块的结构体类型:
struct-decl = [modifier-list] 'extern' 'struct' [identifier] [generic-params-decl] [':' bases-clause] ['=' simple-type-spec] ';'4. 链接期 export 类型别名声明—— 以类型别名形式导出结构体:
struct-decl = [modifier-list] 'export' 'struct' [identifier] [generic-params-decl] [':' bases-clause] '=' simple-type-spec ';'成员列表文法:
member-list = ( var-decl | type-decl | function-decl | constructor-decl | property-decl | subscript-op-decl | function-call-op-decl )*参数说明
| 语法单元 | 含义 |
|---|---|
modifier-list | 可选的修饰符列表,包含访问控制(public/internal/private)与属性(attribute) |
identifier | 结构体类型的可选名称 |
generic-params-decl | 可选泛型参数声明,见 generics |
bases-clause | 可选的继承接口列表,见 types-interface |
simple-type-spec | 可选的别名类型表达式,用于模块相关语法 |
where-clause | 可选的泛型约束表达式,见 generics |
member-list | 结构体成员列表,成员可以是成员变量、嵌套类型、成员函数、构造函数、属性、下标运算符或函数调用运算符 |
已废弃语法:struct关键字后的中括号属性列表
⚠️Warning:在
struct关键字之后使用中括号属性列表的语法已在 Slang 2025 中被弃用,并在 Slang 2026 中移除。请将中括号属性放在struct关键字之前的modifier-list中(参见 Issue #9691)。
即:正确写法是[attribute] struct Foo { ... },而非struct [attribute] Foo { ... }。
语义总览(Description)
一个结构体(structure)是由一组有序成员构成的类型,其四种声明形式分别对应不同的使用场景:
- no-body 声明:仅指定某名称的结构体类型存在,使其可以出现在类型表达式中(无需立即给出成员定义);
- with-members 声明:定义结构体类型,包含布局及可扩展(见 types-extension)的非布局成员列表;
- extern 声明:指定一个在其他模块中定义的结构体类型;
- export 声明:将一个结构体以类型别名导出。
命名结构体与匿名结构体
当identifier指定时,它是结构体的名称;否则结构体为匿名结构体,编译器会为其分配一个不指定的唯一名称。匿名结构体的主要用途是内联类型定义表达式,例如:
struct { int a; } obj; // obj 的类型是一个匿名结构体,含字段 int a;匿名结构体仅在 with-members 形式下有意义。一致性测试 struct-anonymous.slang 验证了内联匿名结构体的字段可通过点运算符访问,并且在各后端(HLSL/GLSL/Metal/WGSL/CUDA/CPP)下均以SLANG_anonymous的名称发射。
成员种类
结构体成员在结构体体内声明,共九种:
- 静态数据成员:以
static关键字声明的变量; - 非静态数据成员(字段):以普通变量声明;
- 构造函数;
- 静态成员函数:带
static修饰符的函数; - 非静态成员函数:不带
static修饰符的函数; - 嵌套类型:类型或类型别名声明;
- property 声明;
__subscript声明;- 函数调用运算符声明。
static 关键字的存储语义
数据成员和成员函数都可以用static关键字声明:
- 静态数据成员的存储分配自全局存储区(global storage);静态成员函数可以:
- 访问结构体的静态数据成员;
- 调用结构体的其他静态成员函数。
- 非静态数据成员的存储分配为结构体实例的一部分;非静态成员函数可以:
- 同时访问静态与非静态数据成员;
- 同时调用静态与非静态成员函数。
一致性测试 struct-static-member-functional.slang 验证了静态数据成员在 HLSL 中发射为模块作用域静态变量(C7);struct-static-const-initializer.slang 验证了static const数据成员带默认初始化器时可经类型名访问并被常量折叠(C8)。
默认初始化器规则
数据成员可以携带默认初始化器,规则如下:
- 使用初始化列表初始化对象时,若列表未提供某非静态数据成员的值,则使用该成员的默认初始化器;
- 使用构造函数初始化对象时,默认初始化器指定数据成员的初始值;构造函数可以覆盖该值(
const成员除外); static const数据成员必须带有默认初始化器。
一致性测试 struct-default-field-initializer.slang 验证 C22(初始化列表场景),struct-default-initializer-constructor-override.slang 验证 C23(构造函数覆盖场景)。
顺序分配、嵌套类型、接口实现与扩展
- 非静态数据成员在结构体类型变量被分配时按声明顺序顺序分配;
- 嵌套类型是封闭在外层结构体作用域内的普通类型,通过外层结构体名访问;
- 结构体可以实现一个或多个 interface 类型;
- 结构体可以通过 type extension 被扩展。
一致性测试 struct-nested-type.slang 验证 C9(嵌套类型访问),struct-interface-conformance.slang 验证 C11(接口实现后可传入受该接口约束的泛型),struct-generic.slang 验证带<T>泛型参数的结构体可在int/float上实例化。
访问控制
struct成员可以通过modifier-list中的访问控制说明符public、internal、private声明可见性:
- 默认成员可见性为
internal; - 从语言版本2026起,无显式说明符的成员继承其外层
struct的可见性(因此public结构体的未修饰成员为public); - 嵌套
struct的成员可以访问外层struct的private成员。
一致性测试 struct-access-visibility-default-internal.slang 验证了 C12:显式public字段与默认可见性字段在public struct中均可从外部访问,且二者在 HLSL/GLSL/SPIR-V/Metal/WGSL/CUDA/CPP 各后端发射时均保留为结构体成员。
⚠️Warning:struct 继承 struct(结构体之间的继承)在 Slang 2025 及更早的语言版本中不稳定,并已在 Slang 2026 中移除。请改用组合(将结构体作为成员)来代替。
对象(Objects)
对象(object)是struct的一个实例。一个实例由该结构体定义的所有非静态数据成员组成。数据成员可以通过初始化列表或构造函数初始化,详见 变量声明。
一致性测试 struct-aggregate-init.slang 验证 C13/C14:实例字段是每对象一份,初始化列表在构造时设置字段值。
构造函数(Constructors)
语法
接口中使用的无函数体声明:
'__init' [generic-params-decl] '(' param-list ')' ('where' where-clause)* ';'带函数体的声明:
'__init' [generic-params-decl] '(' param-list ')' ('where' where-clause)* '{' body-stmt* '}'语义规则
- 当结构体定义了用户提供的构造函数时,对象实例化时会执行构造函数;
- 构造函数可以有任意数量的参数;
- 构造函数没有返回类型;
- 可以定义多个构造函数,此时根据初始化参数执行重载决议选出最合适的一个;
- 构造函数参数通过可选的初始化列表提供;未提供初始化列表时调用无参构造函数;
- 若某非静态数据成员未被构造函数初始化,则对象实例化后其状态未定义;
const数据成员不能被构造函数初始化;where-clause为可选的泛型约束表达式。
示例(来自语言参考):
struct TestClass { int a, b; __init() { a = 1; b = 2; } __init(int _a) { a = 1; b = 2; } __init(int _a, int _b) { a = _a; b = _b; } } TestClass obj1; // obj1.a = 1; // obj1.b = 2; // // Note: TestClass obj1 = { }; also calls the constructor // without parameters TestClass obj2 = { 42 }; // obj2.a = 42; // obj2.b = 2; TestClass obj3 = { 42, 43 }; // obj3.a = 42; // obj3.b = 43;一致性测试 struct-constructor-overload.slang 完整验证了 C15/C16/C17:三个__init重载(无参、单参、双参)在-cpu运行时按参数个数正确分派,且在 HLSL/GLSL/Metal/WGSL/CUDA/CPP 上均发射为TestClass_x24init_N形式的独立构造函数。
聚合初始化(无用户构造函数时)
当没有定义用户提供的构造函数时,执行聚合初始化:
- 初始化列表按顺序为结构体的非静态数据成员提供值;
- 若初始化列表提供的值不足,剩余数据成员被默认初始化;
- 若完全没有提供初始化列表,则无用户构造函数的类以未定义状态实例化。
一致性测试 struct-aggregate-init.slang 验证 C20/C21:Plain p1 = { 10, 20, 30 };全量填充;Plain p2 = { 5, 6 };第三个字段z被默认初始化为 0(CHECK 输出为 10/20/30/5/6/0)。
📝Remark 1:当无用户构造函数的类在无初始化列表的情况下实例化时,对象初始状态未定义。这包括其数据成员本身带有用户提供构造函数的成员(子结构体
TestField的情况)。外层结构体的字段状态仍是未定义的,无论TestField是否有用户构造函数。struct TestField { int x; __init() { x = 5; } } struct TestClass { int a, b; TestField f; } // note: obj is instantiated with an undefined state // regardless of TestField having a user-provided constructor. TestClass obj;
📝Remark 2:访问处于未定义状态的数据成员属于未定义行为。
负向测试:const 成员不可被构造函数初始化
一致性测试 struct-constructor-no-const-member.slang 验证 C19:在__init内给const数据成员赋值是编译错误(E30011)。
静态成员函数(Static Member Functions)
- 静态成员函数是封闭在结构体名字空间内的普通函数;
- 静态成员函数只能访问静态结构体成员;
- 调用静态成员函数不需要对象。
一致性测试 struct-static-member-function.slang 验证 C24/C25:静态成员函数无需对象即可调用,并在 HLSL 中发射为自由函数。
非静态成员函数(Non-static Member Functions)
- 非静态成员函数有一个隐藏参数
this,它引用对象;该隐藏参数用于引用对象数据成员以及调用其他非静态成员函数; - 函数体内,其他成员可以使用
this.引用,但可省略; - 默认情况下,成员函数对对象成员只有只读访问权限;若需写访问,成员函数必须声明
[mutating]属性; - 非静态成员函数不能在没有对象的情况下调用。
📝Remark:用 C++ 术语来说,成员函数默认是
const的。[mutating]属性使其变为非const成员函数。
综合示例(来自语言参考):
struct TestStruct { // data member int a; // static data member, initial value 5 static int b = 5; // static constant data member, initial value 6 static const int c = 6; // nested type struct NestedStruct { static int c = 6; int d; } // member function with read-only access int getA() { // also just plain "return a" would do return this.a; } // member function with read/write access [mutating] int incrementAndReturnA() { a = a + 1; return a; } // static member function static int getB() { return b; } static int incrementAndReturnB() { // [mutating] not needed for modifying static member b = b + 1; return b; } } TestStruct obj = { }; TestStruct.NestedStruct obj2 = { }; obj.a = 42; int tmp0 = TestStruct.b + TestStruct.NestedStruct.c; int tmp1 = obj.getA(); int tmp2 = obj.incrementAndReturnA(); int tmp3 = TestStruct.getB(); int tmp4 = TestStruct::incrementAndReturnB();注意:
::等价于.用于静态成员访问,但推荐使用.(::已废弃)。
一致性测试对[mutating]的验证
- struct-this-implicit-optional.slang 验证 C26:
this.member与裸member访问同一字段; - struct-member-function-readonly.slang 验证 C27:非
[mutating]成员函数读取字段正确; - struct-mutating-member-function.slang 验证 C28:
[mutating]成员函数写入非静态字段,且变异在实例上可观察;从源码可见,该测试还在各后端验证了this参数的类型限定符——HLSL/GLSL 发射为inout TestStruct this_N,Metal 为thread*,WGSL 为ptr<function, ...>,CUDA/CPP 为指针; - struct-mutating-rejected-without-attribute.slang 验证 C29:在非
[mutating]函数中写入非静态成员触发编译错误E30011(非 l-value)。
属性(Properties)
语法
现代语法、隐式get声明(仅接口中):
'property' identifier ':' simple-type-spec ';'现代语法、显式访问器声明:
'property' identifier ':' simple-type-spec '{' accessor-decl* '}'传统语法、隐式get声明(仅接口中):
'property' traditional-var-decl ';'传统语法、显式访问器声明:
'property' traditional-var-decl '{' accessor-decl* '}'访问器声明语法,无函数体(仅接口中):
accessor-decl = ('get' | 'set') ['(' param-list ')'] ';'访问器声明语法,带函数体:
accessor-decl = ('get' | 'set') ['(' param-list ')'] '{' body-stmt* '}'语义规则
- property 是非静态成员,提供数据成员访问接口;
- 读取 property 时定向到
get访问器,写入时定向到set访问器; - 只读 property仅有
get访问器;只写 property仅有set访问器;读写 property两者都有; get访问器声明中的括号可选,get不接受参数;set访问器声明中的括号和参数可选;未指定参数时,set体内自动提供与 property 同类型的隐式参数newValue;- 无访问器或访问器无函数体的 property 声明形式仅在 interface 声明中有用。
⚠️Warning:property 引用访问器
ref是 Slang 内部语言特性,可能发生变化,使用时可能无法按预期工作。
示例(来自语言参考):
struct TestClass { float m_val; bool m_valIsPositive; property someProp : float { get { return m_val; } set { m_val = newValue; m_valIsPositive = (newValue > 0.0f); } } } [shader("compute")] void main(uint3 id : SV_DispatchThreadID) { TestClass obj = { }; // this sets both obj.m_val and obj.m_valIsPositive obj.someProp = 3.0f; }📝Remark 1:当希望系统化地在数据成员访问上附加额外逻辑时,property 可用来替代非
static数据成员,从而避免重构调用点。📝Remark 2:非静态数据成员可以用来实现接口的 property 要求,详见 interfaces。
📝Remark 3:上例中 property 也可声明为
get()/set(float newVal)的显式参数形式(见 struct-property-explicit-param.slang)。
一致性测试验证
- struct-property-get-set.slang 验证 C30/C32:读走
get、写走set且set体内使用隐式newValue;运行输出确认m_val与m_valIsPositive副作用一致(3 / 1 / 0 / -1); - struct-property-readonly.slang 验证 C31:仅
get的只读 property 在 HLSL 中正确发射; - struct-property-explicit-param.slang 验证 C33:显式参数名在
set体内替代newValue。
成员与嵌套类型的访问
静态/非静态成员及嵌套类型均使用.运算符访问。
⚠️Warning:C++ 风格的作用域解析运算符
::已废弃,不应再使用。
一致性测试 struct-dot-member-access.slang 验证 C34(点运算符访问各类成员与嵌套类型),struct-members-full-example-emission.slang 将语言参考中的完整综合示例发射到 HLSL 验证可编译性(C35 的::废弃告警行为因规范未明示告警代码而未纳入测试)。
下标运算符(Subscript Operator)
语法
隐式get声明(仅接口中):
'__subscript' '(' param-list ')' '->' simple-type-spec ';'显式访问器声明:
'__subscript' '(' param-list ')' '->' simple-type-spec '{' accessor-decl* '}'accessor-decl语法与 property 相同。
语义规则
- 结构体可通过
__subscript声明添加下标[]运算符; - 概念上与
property类似,主要区别是它作用于struct 实例本身(而非成员),并且接受参数; - 下标声明可以有任意数量的参数,包括零个参数;
- 读取时调用
get访问器(get声明中的括号可选);赋值时调用set访问器(括号和参数可选,未指定时提供隐式newValue,类型与下标返回类型相同); - 允许多个
__subscript声明,只要签名不同;重载决议与函数调用一致。
⚠️Warning:下标运算符引用访问器
ref是 Slang 内部语言特性,可能发生变化,使用时可能无法按预期工作。
示例(来自语言参考):
RWStructuredBuffer<float> outputBuffer; struct TestStruct { var arr : float[10][10]; // 0-parameter subscript operator __subscript () -> float { get { return arr[0][0]; } set { arr[0][0] = newValue; } } // 1-parameter subscript operator __subscript (int i) -> float { get { return arr[0][i]; } set { arr[0][i] = newValue; } } // 2-parameter subscript operator __subscript (int i0, int i1) -> float { get { return arr[i1][i0]; } set { arr[i1][i0] = newValue; } } } [numthreads(1,1,1)] void main(uint3 id : SV_DispatchThreadID) { TestStruct x = { }; x[] = id.z; x[id.y] = id.z; x[id.x, id.y] = id.z; outputBuffer[id.x] = x[]; outputBuffer[id.y] = x[id.x]; outputBuffer[id.z] = x[id.x, id.y]; }一致性测试 struct-subscript-multi-param.slang 验证 C37/C38:零参数与单参数__subscript重载共存,m[]与m[1]分别正确分派(输出 100 / 200);struct-subscript-op-functional.slang 验证 C36 的读写路径。
函数调用运算符(Function Call Operator)
语法
无函数体声明(仅接口中):
simple-type-spec 'operator' '(' ')' '(' param-list ')' ';'带函数体声明:
simple-type-spec 'operator' '(' ')' '(' param-list ')' '{' body-stmt* '}'语义规则
- 通过
operator ()声明可以添加函数调用()运算符,使对象可以像函数一样被调用; - 允许多个声明,只要签名不同;重载决议与函数调用一致。
示例(来自语言参考):
RWStructuredBuffer<float> outputBuffer; struct TestStruct { float base; float operator () () { return base; } float operator () (uint x) { return base * float(x); } float operator () (uint x, uint y) { return base * float(x) * float(y); } } void main(uint3 id : SV_DispatchThreadID) { TestStruct obj = { 42.0f }; outputBuffer[0] += obj(); outputBuffer[0] += obj(id.y); outputBuffer[0] += obj(id.z, id.z * 2); }一致性测试 struct-call-op-functional.slang 验证 C39/C40:多个operator ()重载按参数个数正确分派。
内存布局(Memory Layout)
自然布局(Natural Layout)
自然布局是结构体类型的基准布局,规则如下:
- 结构体的对齐是 1、任意成员对齐、任意父类型对齐三者的最大值;
- 数据按如下顺序排列:父类型 → 非静态数据成员;
- 数据项的偏移:第一项偏移为 0;第N项的偏移 = 前一项的偏移+大小,再向上取整到该项的对齐;
- 结构体的大小 = 最后一项的偏移+大小,即不做尾填充(不向上取整到结构体对齐)。
以下算法可实现自然布局:
1. 初始化变量 size = 0,alignment = 1 2. 对结构体类型的每个字段 f: a. alignment = max(alignment, f 的对齐) b. size = 不小于 size 的 alignment 的最小倍数 c. 字段 f 的偏移 = size d. size += f 的大小 3. 完成后,size 与 alignment 即结构体类型的大小与对齐📝Remark:大多数目标平台并不直接使用自然布局,但它为定义其他布局提供了基线。任何结构体布局都必须保证对齐至少不小于标准布局。
一致性测试 struct-natural-layout-emission.slang 验证 C41/C10:int x; float y; int z;在 HLSL/GLSL/Metal/WGSL/CUDA/CPP 均按声明顺序发射;SPIR-V 层面还验证了OpMemberName索引 0/1/2 与OpTypeStruct %int %float %int的成员顺序(在-O0下钉住,避免优化折叠)。
C 风格布局(C-Style Layout)
C 风格布局与自然布局的区别在于:结构体大小向上取整到结构体对齐(即增加尾填充),这与典型 C/C++ 编译器的布局规则一致。
注意:该行为无法仅通过 slangc CLI 观察(需要反射 API 查询结构体大小与对齐),因此一致性套件将其标记为
needs-unit-test未纳入运行时测试(见 README 中 C42 的说明)。
D3D 常量缓冲区布局(D3D Constant Buffer Layout)
D3D 常量缓冲区布局与自然布局类似,但有两点差异:
- 最小对齐为 16;
- 若某数据成员跨过 16 字节边界且其偏移未按 16 对齐,则将该偏移向上取整到下一个 16 的倍数——在 HLSL 中这被称为improper straddle(不当跨越)。
注意:D3D 布局细节需要特定 GPU 层级的测试,且字节偏移/对齐数值无法仅从 slangc CLI 观察(需反射 API 或 cbuffer 布局查询),因此一致性套件未在运行时测试中覆盖(README 中 C43 的说明)。
This 类型关键字
在结构体或接口声明的函数体内,关键字This可用于引用封闭类型:
- 在结构体类型声明内,
This指结构体类型本身; - 在接口声明内,
This指符合该接口的具体类型(即this的类型)。
一致性测试 struct-This-type-keyword.slang 验证 C44:成员函数的返回类型与局部变量均使用This,运行输出确认返回的实例携带期望值(CHECK: 99),并在各后端正确发射。
一致性测试套件总览与未覆盖项
README.md 记录了该一致性套件的完整方法论:
- 覆盖策略:每个语言参考声明(claim)配一个功能测试(INTERPRET 运行时或 emit 发射测试),沿文档标注的维度展开(成员种类、可见性、构造重载、访问器形式、下标元数、调用运算符重载、内存布局、泛型结构体、接口实现、嵌套类型、
This关键字);负向测试覆盖“向只读成员写入”与“构造函数中写 const 成员”; - 静态数据成员:仅通过 HLSL 发射测试,因为 slangi VM 字节码发射器不支持模块作用域全局指令(
slangi-vm-global-var-bytecode-unsupported); - 未测试声明及其原因:
- C2/C5(
extern struct/export struct)需要跨文件模块链接(needs-multi-file-test); - C18(构造函数未初始化成员为未定义状态)属未定义行为无法断言;
- C42/C43(布局数值)需要反射 API 或单元测试(
needs-unit-test); - C35(
::废弃告警)行为未在规范中明示; where子句泛型由 generics 套件覆盖,ref访问器因属内部特性不纳入测试。
- C2/C5(
文档缺口(Doc gaps observed)
README 还记录了撰写该套件时发现的文档缺口:
- no-body 声明与后续定义:语言参考称 no-body 声明“使类型可先用于类型表达式”,但编译器并不将 no-body 声明与同名 with-members 声明合并——
struct ForwardDeclared;后跟struct ForwardDeclared { int x; }会报error[E30200] conflicting declaration及error[E39999] ambiguous reference(hlsl/glsl/spirv-asm/metal/wgsl 行为一致); This小节缺少稳定锚点:文档末尾 "This Type" 小节没有独立的#this-type锚点,无法被 doc_ref 精确引用;- 构造函数未定义状态的示例缺失:文档 Remark 1 缺少一个“外层无构造函数、内层字段类型带
__init”的独立最小示例。
结语
从语言参考 types-struct.md 到一致性测试 conformance/types-struct,本文完整覆盖了 Slang 结构体的全部规范维度:四种声明形式、九类成员、static存储语义、初始化与聚合初始化、构造重载、this/[mutating]读写规则、property/subscript/call-op 三类访问器成员、三种内存布局算法、访问控制与This关键字。每个规范点都有对应的源码级测试证据(如E30011l-value 错误、inout/thread*/ptr<function,...>的this参数发射差异、SPIR-VOpTypeStruct成员顺序),既可作为编写 Slang 着色器的速查手册,也可作为阅读编译器行为的入口参考。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考