Slang 结构体(struct)语言参考与一致性测试全解析
2026/9/19 4:36:30 网站建设 项目流程

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的名称发射。

成员种类

结构体成员在结构体体内声明,共九种:

  1. 静态数据成员:以static关键字声明的变量;
  2. 非静态数据成员(字段):以普通变量声明;
  3. 构造函数
  4. 静态成员函数:带static修饰符的函数;
  5. 非静态成员函数:不带static修饰符的函数;
  6. 嵌套类型:类型或类型别名声明;
  7. property 声明
  8. __subscript声明
  9. 函数调用运算符声明

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中的访问控制说明符publicinternalprivate声明可见性:

  • 默认成员可见性为internal
  • 从语言版本2026起,无显式说明符的成员继承其外层struct的可见性(因此public结构体的未修饰成员为public);
  • 嵌套struct的成员可以访问外层structprivate成员

一致性测试 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、写走setset体内使用隐式newValue;运行输出确认m_valm_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访问器因属内部特性不纳入测试。

文档缺口(Doc gaps observed)

README 还记录了撰写该套件时发现的文档缺口:

  1. no-body 声明与后续定义:语言参考称 no-body 声明“使类型可先用于类型表达式”,但编译器并不将 no-body 声明与同名 with-members 声明合并——struct ForwardDeclared;后跟struct ForwardDeclared { int x; }会报error[E30200] conflicting declarationerror[E39999] ambiguous reference(hlsl/glsl/spirv-asm/metal/wgsl 行为一致);
  2. This小节缺少稳定锚点:文档末尾 "This Type" 小节没有独立的#this-type锚点,无法被 doc_ref 精确引用;
  3. 构造函数未定义状态的示例缺失:文档 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),仅供参考

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

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

立即咨询