Slang 指针类型(Pointer Types)全面指南:声明语法、泛型指针、指针运算与并发测试验证
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
导读
本文以 Slang 语言参考文档 types-pointer.md 为骨架,系统讲解 Slang 中指针类型的声明语法(T*)、标准库泛型指针(Ptr<T>/ImmutablePtr<T>)、地址取址与解引用、指针算术、指针 traits(访问模式与地址空间)以及当前的实现限制。文中同时引入 docs/generated/tests/conformance/types-pointer 下的一组一致性测试(conformance tests),以真实的.slang测试用例印证文档中的每一条声明。读者读完本文后,将能准确写出可编译、可运行的 Slang 指针代码,理解指针在 CPU/CUDA 目标上的行为边界,并能够看懂仓库中的指针相关测试是如何组织与断言(CHECK)的。
注意:Slang 文档明确标注指针在
slangc中尚未完全实现("Pointers are not yet fully implemented inslangc"),因此本文所述内容以当前仓库文档与测试所确认的范围为准。
声明语法(Declaration Syntax):T*
语法形式与类型语义
指针类型通过在类型说明符后附加'*'声明。语言参考中给出的类型说明符语法为:
simple-type-id-spec = [modifier-list] type-identifier [generic-params-decl] ( '[' [constant-index-expr] ']' | '*' )*其中:
modifier-list:可选的修饰符列表;type-identifier:命名现有类型或泛型类型的标识符,可以是基础类型、向量/矩阵泛型类型、用户自定义的结构体类型、接口类型、枚举类型、类型别名或模块提供的类型;generic-params-decl:泛型参数声明;'[' constant-index-expr ']':数组维度声明;'*':指针声明,即本主题。
语义要点:
- 指针类型
T*表示"指向类型T的对象"的地址。 - 基类型可以是任何 addressable(可寻址)类型,包括指针类型与数组类型(即支持"指向指针的指针"、"指向数组的指针"等嵌套形式)。
- Remark 1(等价关系):声明语法写出的指针类型等价于泛型指针类型
Ptr<T, Access.ReadWrite, AddressSpace.Device>。 - Remark 2:指针也可以通过变量声明来声明——此时变量本身被声明为指针,而非类型本身是指针类型。
测试验证:T*声明、取址与解引用往返
一致性测试 declare-star-syntax-functional.slang 以INTERPRET(解释执行)模式验证声明、取址、解引用三者协同工作:
//TEST:INTERPRET(filecheck=CHECK): void main() { int x = 42; int* p = &x; //CHECK: 42 printf("%d\n", *p); }测试通过filecheck=CHECK断言*p的输出为42,证明int* p = &x完整覆盖了"声明指针类型 → 取址 → 解引用"的往返链路。
T*与Ptr<T, Access.ReadWrite, AddressSpace.Device>类型等价
测试 ptr-star-equals-ptr-generic-functional.slang 进一步验证 Remark 1 的类型等价性——T*值可以直接赋值给全限定形式的Ptr<>变量,无需任何转换:
//TEST:INTERPRET(filecheck=CHECK): void main() { int x = 42; int* p1 = &x; Ptr<int, Access.ReadWrite, AddressSpace.Device> p2 = p1; //CHECK: 42 42 printf("%d %d\n", *p1, *p2); }两个指针解引用得到相同结果,印证两种拼写是同一类型。
泛型指针类型(Generic Pointer Types)
Slang 标准库提供两种泛型指针类型别名:
| 别名 | 语义 | 参数 |
|---|---|---|
Ptr<T, AccessMode, AddressSpace> | 通用指针类型 | T:元素类型;AccessMode:存储访问模式;AddressSpace:存储地址空间 |
ImmutablePtr<T, AddressSpace> | 指向不可变数据的指针类型别名 | T:元素类型;AddressSpace:存储地址空间 |
关于AccessMode与AddressSpace的取值详见下文 指针 traits 一节。
Ptr<T>的声明与解引用
测试 ptr-generic-type-functional.slang 验证Ptr<T>别名可声明、可从&obj赋值并可解引用:
//TEST:INTERPRET(filecheck=CHECK): void main() { int x = 55; Ptr<int> p = &x; //CHECK: 55 printf("%d\n", *p); }ImmutablePtr<T>只读指针
测试 immutableptr-type-functional.slang 验证只读别名:由于没有隐式转换(见 指针 traits),需要通过显式 cast 从T*得到ImmutablePtr<int>,随后可以正常解引用读取:
//TEST:INTERPRET(filecheck=CHECK): void main() { int x = 33; ImmutablePtr<int> ro = (ImmutablePtr<int>)(&x); //CHECK: 33 printf("%d\n", *ro); }描述(Description):取址、解引用与指针算术
地址获取:&与__getAddress
取址运算符&用于获取对象的地址,可以赋给类型匹配的指针变量;标准库内建函数__getAddress(obj)是&的替代写法。
- 测试 address-of-operator-functional.slang 验证
&a产生可用指针、解引用还原原值(*(&obj) == obj)。 - 测试 getaddress-builtin-functional.slang 验证
__getAddress(obj)产生同样的有效指针。
解引用:读与写
解引用运算符*访问(读取)被指向的对象,并且*p是l-value——通过*p = v可以改变被指向对象的值。
测试 dereference-write-functional.slang 验证写操作对原变量可见:
//TEST:INTERPRET(filecheck=CHECK): void main() { int x = 10; int* p = &x; *p = 99; //CHECK: 99 printf("%d\n", x); }指针作为函数参数传入时,被调函数可通过解引用修改调用方变量。测试 pointer-pass-to-function-functional.slang 验证了这一点:
void addDelta(int* p, int delta) { *p = *p + delta; } void main() { int x = 10; addDelta(&x, 5); //CHECK: 15 printf("%d\n", x); }成员访问:p->member
当指针指向结构体或类类型时,可以使用成员访问运算符.或->解引用并访问成员。测试 struct-arrow-member-access-functional.slang 验证p->member与(*p).member访问同一成员,且通过->的修改对原对象可见(C10)。
指针算术:+、-、++、--
当指针指向数组元素时,可以对指针加/减整数值,得到的指针指向偏移后的元素:
p + n(C11):向前移动n个元素。测试 pointer-arithmetic-add-functional.slang 验证解引用结果正确;pointer-arithmetic-spirv-emission.slang 验证发射(emission)形态(见下文)。p - n(C12):向后移动n个元素。测试 pointer-arithmetic-sub-functional.slang 验证。++p/--p(C13):前进/后退一个元素。测试 pointer-increment-functional.slang 验证。
各后端指针算术的发射形态
pointer-arithmetic-spirv-emission.slang 使用运行时偏移(从 buffer 读取偏移量以防止常量折叠),验证各后端的 lowering 结果:
//TEST:SIMPLE(filecheck=CHECK):-target spirv-asm -entry main -stage compute //TEST:SIMPLE(filecheck=CHECKCPP):-target cpp -entry main -stage compute //TEST:SIMPLE(filecheck=CHECKCUDA):-target cuda -entry main -stage compute [numthreads(1,1,1)] void main(RWStructuredBuffer<int> inputBuf, RWStructuredBuffer<int> outputBuf) { int arr[4]; arr[0] = inputBuf[0]; arr[1] = inputBuf[1]; arr[2] = inputBuf[2]; arr[3] = inputBuf[3]; int* p = &arr[0]; p = p + inputBuf[4]; // runtime offset — forces OpPtrAccessChain outputBuf[0] = *p; } // CHECK: OpPtrAccessChain // CHECKCPP: &arr_0[int(0)] + // CHECKCUDA: &arr_0[int(0)] +关键断言:
- SPIR-V:本地数组上的运行时指针算术发射为
OpPtrAccessChain; - CPP / CUDA:发射为显式的指针加法表达式(
&arr_0[int(0)] + ...)。
空指针与"末尾之后"哨兵
- 空指针(C14):
nullptr是合法的空指针值,p == nullptr对空指针为真、对非空指针为假。测试 null-pointer-functional.slang 验证。 - 末尾之后哨兵(C15):
&arr[N-1] + 1是合法的"数组最后一个元素之后"的指针,可作为哨兵值参与比较,但不可解引用。
语言参考还给出了一段指针算术合法性示例(Remark 2):
var arr : uint[10] = { }; var ptr : uint *; ptr = &arr[9]; // OK: ptr points to the last element // of the array ptr++; // Still OK: ptr points to one past the // last element ptr++; // Pointer is now invalid ptr--; // No validity guarantees with invalid // pointers in pointer expressions; // dereferencing would be undefined behavior指针值的四类归属与未定义行为
文档将指针值划分为四类:
- 指向具有匹配 traits 的对象的指针(包括指向数组元素的指针);
- 指向对象末尾之后的指针;
- 空指针(指向任何对象的特殊指针值);
- 无效指针(其余情况)。
解引用不指向"具有匹配 traits 的对象"的指针是未定义行为(undefined behavior)。另外文档特别警告:当指针指向多维数组中的元素时,指针算术必须始终停留在同一最内层数组中,或得到"指向该数组最后一个对象之后"的指针(该指针不可解引用),其他任何结果都是未定义行为。
指针 Traits(Pointer Traits)
指针类型具有以下 traits:
- 被指向对象的类型;
- 访问模式(access mode);
- 地址空间(address space)。
一个有效指针只能指向 traits 匹配的对象。
默认值:Device 地址空间 + ReadWrite 访问模式
默认指针地址空间是AddressSpace.Device,默认访问模式是Access.ReadWrite;使用声明语法无法指定其他地址空间或访问模式。测试 pointer-default-access-mode-functional.slang 验证了这一默认行为(C16)。
其他地址空间与访问模式的指针,需通过标准库类型别名Ptr<T, AccessMode, AddressSpace>声明。
禁止隐式转换:读写 → 只读
不存在从读写指针(Ptr<T>)到只读指针(ImmutablePtr<T>)的隐式转换。测试 no-implicit-rw-to-immutable-negative.slang 以负向诊断测试(DIAGNOSTIC_TEST)断言编译器以错误码E30019拒绝隐式赋值:
//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK,non-exhaustive):-target cpp void main() { int x = 10; Ptr<int> rw = &x; ImmutablePtr<int> ro = rw; //CHECK: E30019 }如需只读指针,必须使用显式 cast(参见上文ImmutablePtr<T>一节)。
当前限制与注意点
文档明确列出了指针尚未完全实现的范围,一致性测试也对此进行了验证与记录:
- 局部内存指针仅支持 CPU 与 CUDA 目标。
operator&在核心模块中被门控在cpp_cuda_spirv_llvm能力集上;对 Vulkan/D3D12 等 GPU 目标编译包含本地内存取址的着色器会以error[E36107]: unavailable features in entry point中止(需 GPU 驱动环境才能验证,CI 会覆盖)。 - 不支持指向不透明句柄类型(如
Texture2D)的指针;句柄指针应使用DescriptorHandle<T>。测试中发现一个文档缺口(doc gap):对全局作用域的Texture2D取址实际报错为 E30079("cannot take address of immutable object"),而不是专门的"不支持不透明句柄指针"诊断——原因是资源类型是隐式不可变对象。详见一致性测试 README 的 "Doc gaps observed" 一节。 - 不支持
const指针。const T*声明被编译器以错误码E20017拒绝,负向测试 const-pointer-rejected-negative.slang 固定了该诊断;文档建议改用Ptr<T, Access.Read>或let/const Ptr<...>表达只读语义。 - 不支持自定义对齐规格;对于已知对齐的指针加载/存储,应使用标准库函数 loadAligned() /
storeAligned()(这些函数定义在本文档之外的标准库中)。 - 并非所有目标都支持指针。发射测试 declare-star-syntax-emission.slang 展示了各后端的实际表现:
- CPP:发射
int32_t等原生指针类型(CHECK: int32_t); - CUDA:发射
__global__内核标记; - SPIR-V asm:发射
OpTypePointer StorageBuffer(存储缓冲指针类型); - Metal:设备内存指针发射为
int device* devPtr; - GLSL:指针以
GL_EXT_buffer_reference块形式渲染(layout(buffer_reference, std430, buffer_reference_align = 4) buffer BufferPointer_...); - HLSL / WGSL被排除在发射 fan-out 之外:D3D 没有指针类型,
slangc会把 Slang 内部的Ptr<int, ...>泛型泄漏到 HLSL 输出中;WGSL 的ptr<>要求地址空间,而当前输出中地址空间为空。这两项分别记录为待发现项(findings)_meta/findings/hlsl-pointer-param-emits-internal-ptr-generic.yaml与_meta/findings/wgsl-pointer-param-emits-empty-address-space.yaml。
- CPP:发射
- 不支持指针继承。指向符合接口
I的结构体的指针不能 cast 为指向I的指针。测试记录了一个文档缺口:编译器在 parse/check 阶段接受(IFoo*)bp这种显式 cast,失败只会在 code-gen 阶段尝试接口动态派发时以E50100出现——即被拒绝的是派发而非 cast 本身。
实战示例:用指针表示半开区间[start, end)
语言参考的 Examples 一节演示了指针最典型的实战用法——用一对指针表示半开区间并求和。以下完整示例可直接放入计算着色器:
RWStructuredBuffer<uint> outputBuffer; cbuffer Globals { Ptr<uint> g_inputData; uint g_inputDataLen; } // Calculate sum of half-open range [start, end) uint sumOfValues(uint *start, uint *end) { uint sum = 0; for (uint *i = start; i != end; ++i) { sum = sum + *i; } return sum; } [numthreads(1, 1, 1)] void main(uint3 id : SV_DispatchThreadID) { // Calculate sum of elements 0, 1, ..., 9 provided // the input data buffer is big enough. outputBuffer[id.x] += sumOfValues( g_inputData, &g_inputData[min(g_inputDataLen, 10)]); }这段代码同时覆盖了多项核心能力:
Ptr<uint> g_inputData使用泛型指针别名声明 uniform 数据指针;uint *start, *end使用声明语法;- 通过
&g_inputData[...]取址构造区间终点; - 循环条件
i != end依赖"末尾之后哨兵"的合法比较; ++i使用指针自增;*i解引用读取元素。
一致性测试 pointer-range-iteration-functional.slang 以解释执行方式验证了相同的区间迭代模式——对数组{1,2,3,4,5}以end = &arr[4] + 1为哨兵求和,CHECK 断言结果为15:
uint sumRange(uint* start, uint* end) { uint sum = 0; for (uint* i = start; i != end; ++i) sum = sum + *i; return sum; } void main() { uint arr[5] = { 1, 2, 3, 4, 5 }; uint* start = &arr[0]; uint* end = &arr[4] + 1; // one past last — valid sentinel per doc uint result = sumRange(start, end); //CHECK: 15 printf("%d\n", result); }一致性测试套件总览:18 项声明的验证矩阵
conformance/types-pointer 目录 下的测试套件把语言参考中的指针声明逐条映射为可执行测试(claims 编号 C1–C18),验证方式分为三类:
- functional:功能验证,通常以
TEST:INTERPRET(filecheck=CHECK)解释执行并断言输出; - negative:负向诊断验证,以
DIAGNOSTIC_TEST固定错误码(E20017、E30019、E30079、E50100 等); - boundary:边界行为验证(如空指针、单元素步进、区间哨兵)。
| 声明(Claim) | 意图 | 对应测试文件 |
|---|---|---|
C1.T*声明 +&/*往返正确 | functional | declare-star-syntax-functional.slang、declare-star-syntax-emission.slang |
C2.T*与Ptr<T, Access.ReadWrite, AddressSpace.Device>同类型 | functional | ptr-star-equals-ptr-generic-functional.slang、pointer-default-access-mode-functional.slang |
C3.const T*被 E20017 拒绝 | negative | const-pointer-rejected-negative.slang |
C4.Ptr<T>泛型可声明、可解引用 | functional | ptr-generic-type-functional.slang |
C5.ImmutablePtr<T>别名经显式 cast 可编译解引用 | functional | immutableptr-type-functional.slang |
C6.&obj产生有效指针,*(&obj) == obj | functional | address-of-operator-functional.slang |
C7.__getAddress(obj)是&的替代 | functional | getaddress-builtin-functional.slang |
C8/C9.*p可读,*p = v可写 | functional | dereference-write-functional.slang |
C10.p->member与(*p).member等价 | functional | struct-arrow-member-access-functional.slang |
C11.p + n前进 n 元素 | functional | pointer-arithmetic-add-functional.slang、pointer-arithmetic-spirv-emission.slang |
C12.p - n后退 n 元素 | boundary | pointer-arithmetic-sub-functional.slang |
C13.++p/--p单元素步进 | boundary | pointer-increment-functional.slang |
C14.nullptr空指针比较 | boundary | null-pointer-functional.slang |
| C15/C18. 末尾之后哨兵 + 区间迭代求和正确 | functional | pointer-range-iteration-functional.slang |
| C16. 默认地址空间 Device、默认访问模式 ReadWrite | functional | pointer-default-access-mode-functional.slang |
| C17. 读写 → 只读无隐式转换(E30019) | negative | no-implicit-rw-to-immutable-negative.slang |
| 指针作函数参数可修改调用方变量 | functional | pointer-pass-to-function-functional.slang |
未被测试覆盖的声明(Untested claims)
README 还诚实记录了当前无法通过自动化测试覆盖的声明及其原因,主要包括:
- 局部内存指针仅 CUDA/CPU 支持:验证 GPU 目标拒绝本地内存指针需要真实 GPU 驱动(Vulkan/D3D12),由 CI 覆盖;
- 不支持不透明句柄指针:实际错误是 E30079(关于不可变性)而非专门的"不透明句柄"诊断,属文档措辞不精确;
- 对齐加载/存储:
loadAligned()/storeAligned()定义在本文档之外,无法从声明语法表面直接测试; - 指针到接口的继承 cast:编译器接受显式 cast,仅当 code-gen 阶段尝试接口派发时才以 E50100 失败;
- 未定义行为类声明(解引用无效指针、多维数组越界算术):UB 本质上是规范性的,编译器无需拒绝或标记,按定义不可测试;
- 非 Device 地址空间(如
AddressSpace.GroupShared):需要 GPU 运行环境与特定能力集; - HLSL/WGSL 发射:当前两个后端会泄漏内部泛型或发射空地址空间,属已知缺陷(findings)。
文档缺口记录(Doc gaps)
测试作者在验证过程中发现了两处文档措辞与实际行为不一致之处,均记录在 README 的 "Doc gaps observed" 一节:
- "不支持不透明句柄指针"表述模糊:
&myTex(全局Texture2D)实际报 E30079,读者无法区分错误源于"不透明性"还是"全局不可变性";建议文档说明不透明资源类型是隐式不可变对象,或新增专门诊断。 - "不能 cast 到指向接口的指针"表述不准确:cast 在 parse/check 阶段被接受,失败发生在 code-gen 的接口派发阶段(E50100);建议文档澄清被拒绝的是动态派发而非 cast 本身。
这两处记录对读者极具价值:它们精确划定了文档语义与编译器实现之间的边界。
小结
Slang 的指针类型以T*声明语法为入口,以Ptr<T, AccessMode, AddressSpace>与ImmutablePtr<T, AddressSpace>两个标准库别名扩展了访问模式与地址空间的表达能力。取址(&/__getAddress)、解引用(*,l-value 可写)、成员访问(->)、指针算术(+/-/++/--)、空指针与末尾之后哨兵构成了完整的指针语义;默认AddressSpace.Device+Access.ReadWrite的 traits 规则与"读写 → 只读无隐式转换"(E30019)是类型安全的关键约束。由于slangc尚未完全实现指针,实际使用时务必牢记:局部内存指针仅限 CPU/CUDA,const T*会被 E20017 拒绝,不透明句柄请使用DescriptorHandle<T>,HLSL/WGSL 目标暂不可用。仓库中的一致性测试套件(docs/generated/tests/conformance/types-pointer)为文档的每一项声明提供了可执行、可断言的验证,是理解与排查指针行为的首选参考资料。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考