├── .gitattributes ├── src ├── open_close.rs ├── wgsl_export_macro.rs ├── pasting.rs ├── wgsl_macro.rs ├── sanitize.rs ├── to_wgsl_string.rs └── lib.rs ├── .gitignore ├── Cargo.toml ├── examples ├── naga_oil.rs ├── dedup.rs ├── hello_world.rs └── vertex.rs ├── LICENSE-MIT ├── .github └── workflows │ └── rust.yml ├── README.md └── LICENSE-APACHE /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/open_close.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Delimiter; 2 | 3 | pub fn open(d: Delimiter) -> char { 4 | match d { 5 | Delimiter::Parenthesis => '(', 6 | Delimiter::Brace => '{', 7 | Delimiter::Bracket => '[', 8 | Delimiter::None => ' ', 9 | } 10 | } 11 | 12 | pub fn close(d: Delimiter) -> char { 13 | match d { 14 | Delimiter::Parenthesis => ')', 15 | Delimiter::Brace => '}', 16 | Delimiter::Bracket => ']', 17 | Delimiter::None => ' ', 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgsl_ln" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | authors = ["Mincong Lu "] 7 | license = "MIT OR Apache-2.0" 8 | 9 | readme = "README.md" 10 | repository = "https://github.com/mintlu8/wgsl_ln" 11 | description = """ 12 | Experimental crate for writing wgsl in rust! 13 | """ 14 | keywords = ["wgsl", "shader"] 15 | 16 | [features] 17 | 18 | [profile.dev.package.naga] 19 | debug-assertions=true 20 | 21 | [profile.release.package.naga] 22 | debug-assertions=true 23 | 24 | [lib] 25 | proc-macro = true 26 | 27 | [dependencies] 28 | naga = { version = "25", features = ["wgsl-in"] } 29 | proc-macro-error = "1.0.4" 30 | proc-macro2 = "1.0.86" 31 | quote = "1.0.36" 32 | -------------------------------------------------------------------------------- /examples/naga_oil.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | use wgsl_ln::{wgsl, wgsl_export}; 3 | 4 | #[wgsl_export(Vertex)] 5 | pub static VERTEX: &str = wgsl!( 6 | #import awesome_game_engine::Vertex; 7 | ); 8 | 9 | #[wgsl_export(VertexOutput)] 10 | pub static VERTEX_OUT: &str = wgsl!( 11 | #import awesome_game_engine::{VertexOut} 12 | ); 13 | 14 | pub static VERTEX_SHADER: &str = wgsl!( 15 | @group(#{MATERIAL_BIND_GROUP}) @binding(0) var ratio: f32; 16 | @vertex 17 | fn vertex_shader(vertex: $Vertex) -> $VertexOutput { 18 | var out: $VertexOutput; 19 | out.position = vec4(vertex.position, 0.0); 20 | return out; 21 | } 22 | ); 23 | 24 | pub fn main() { 25 | println!("{}", VERTEX_SHADER); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /examples/dedup.rs: -------------------------------------------------------------------------------- 1 | use wgsl_ln::{wgsl, wgsl_export}; 2 | 3 | #[wgsl_export(sin_cos)] 4 | pub static SIN_COS: &str = wgsl!( 5 | fn sin_cos(v: f32) -> vec2 { 6 | return vec2(sin(v), cos(v)); 7 | } 8 | ); 9 | 10 | #[wgsl_export(sin_cos2)] 11 | pub static SIN_COS_SQUARED: &str = wgsl!( 12 | fn sin_cos2(v: f32) -> vec2 { 13 | return $sin_cos(v) * $sin_cos(v); 14 | } 15 | ); 16 | 17 | pub static SIN_COS_SQUARED_PLUS_SIN_COS_1: &str = wgsl!( 18 | fn a(v: f32) -> vec2 { 19 | return $sin_cos2(v) * $sin_cos(v); 20 | } 21 | ); 22 | 23 | pub static SIN_COS_SQUARED_PLUS_SIN_COS_2: &str = wgsl!( 24 | fn a(v: f32) -> vec2 { 25 | return $sin_cos(v) * $sin_cos2(v); 26 | } 27 | ); 28 | 29 | pub fn main() { 30 | println!("{}", SIN_COS_SQUARED_PLUS_SIN_COS_1); 31 | println!("{}", SIN_COS_SQUARED_PLUS_SIN_COS_2); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mincong Lu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use another_world::{HELLO_2, WORLD_2}; 2 | use the_third_world::WORLD_3; 3 | use wgsl_ln::{wgsl, wgsl_export}; 4 | 5 | #[wgsl_export(hello)] 6 | pub static HELLO: &str = wgsl!( 7 | fn hello(v: vec2) -> f32 { 8 | return (v.x + v.y) + 1.0; 9 | } 10 | ); 11 | 12 | pub static WORLD: &str = wgsl!( 13 | fn world(v: vec4) -> f32 { 14 | return $hello(v.xy) + $hello(v.zw); 15 | } 16 | ); 17 | 18 | mod another_world { 19 | use wgsl_ln::{wgsl, wgsl_export}; 20 | 21 | #[wgsl_export(hello2)] 22 | pub static HELLO_2: &str = wgsl!( 23 | fn hello2(v: vec2) -> f32 { 24 | return $hello(v); 25 | } 26 | ); 27 | 28 | pub static WORLD_2: &str = wgsl!( 29 | fn world(v: vec4) -> f32 { 30 | return $hello(v.xy) + $hello(v.zw); 31 | } 32 | ); 33 | } 34 | 35 | mod the_third_world { 36 | use wgsl_ln::wgsl; 37 | 38 | pub static WORLD_3: &str = wgsl!( 39 | fn world(v: vec4) -> f32 { 40 | return $hello2(v.xy) + $hello(v.zw); 41 | } 42 | ); 43 | } 44 | 45 | pub fn main() { 46 | println!("{}", HELLO); 47 | println!("{}", WORLD); 48 | println!("{}", HELLO_2); 49 | println!("{}", WORLD_2); 50 | println!("{}", WORLD_3); 51 | } 52 | -------------------------------------------------------------------------------- /examples/vertex.rs: -------------------------------------------------------------------------------- 1 | use wgsl_ln::{wgsl, wgsl_export}; 2 | 3 | #[wgsl_export(Vertex)] 4 | pub static VERTEX: &str = wgsl!( 5 | struct Vertex { 6 | @builtin(instance_index) instance_index: u32, 7 | @location(0) position: vec3, 8 | @location(1) normal: vec3, 9 | @location(2) uv: vec2, 10 | @location(3) uv_b: vec2, 11 | @location(4) tangent: vec4, 12 | @location(5) color: vec4, 13 | @location(6) @interpolate(flat) joint_indices: vec4, 14 | @location(7) joint_weights: vec4, 15 | @builtin(vertex_index) index: u32 16 | } 17 | ); 18 | 19 | #[wgsl_export(VertexOutput)] 20 | pub static VERTEX_OUT: &str = wgsl!( 21 | struct VertexOutput { 22 | @builtin(position) position: vec4, 23 | @location(0) world_position: vec4, 24 | @location(1) world_normal: vec3, 25 | @location(2) uv: vec2, 26 | @location(3) uv_b: vec2, 27 | @location(4) world_tangent: vec4, 28 | @location(5) color: vec4, 29 | @location(6) @interpolate(flat) instance_index: u32 30 | } 31 | ); 32 | 33 | pub static VERTEX_SHADER: &str = wgsl!( 34 | @vertex 35 | fn vertex_shader(vertex: #Vertex) -> #VertexOutput { 36 | var out: VertexOutput; 37 | out.position = vec4(vertex.position, 0.0); 38 | return out; 39 | } 40 | ); 41 | 42 | pub fn main() { 43 | println!("{}", VERTEX_SHADER); 44 | } 45 | -------------------------------------------------------------------------------- /src/wgsl_export_macro.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream, TokenTree}; 2 | use proc_macro_error::abort; 3 | use quote::{format_ident, quote}; 4 | 5 | pub fn wgsl_export_macro(attr: TokenStream, stream: TokenStream) -> TokenStream { 6 | let Some(TokenTree::Ident(name)) = attr.into_iter().next() else { 7 | abort!(Span::call_site(), "Expected #[wgsl_export(name)]"); 8 | }; 9 | let mut wgsl_macro_ident = false; 10 | let mut exclamation_mark = false; 11 | let sealed = format_ident!("__sealed_{}", name); 12 | let mut paste = format_ident!("__wgsl_paste_{}", name); 13 | paste.set_span(name.span()); 14 | for token in stream.clone() { 15 | match token { 16 | TokenTree::Ident(i) if i == "wgsl" => { 17 | wgsl_macro_ident = true; 18 | exclamation_mark = false; 19 | } 20 | TokenTree::Punct(p) if wgsl_macro_ident && p.as_char() == '!' => { 21 | exclamation_mark = true; 22 | } 23 | TokenTree::Group(g) if wgsl_macro_ident && exclamation_mark => { 24 | let source = g.stream(); 25 | return quote! { 26 | #[allow(non_snake_case)] 27 | mod #sealed { 28 | #[allow(non_snake_case)] 29 | #[doc(hidden)] 30 | #[macro_export] 31 | macro_rules! #paste { 32 | (wgsl!($($tt: tt)*)) => { 33 | ::wgsl_ln::__wgsl_paste!(#name {#source} $($tt)*) 34 | }; 35 | } 36 | } 37 | #stream 38 | }; 39 | } 40 | _ => { 41 | wgsl_macro_ident = false; 42 | exclamation_mark = false; 43 | } 44 | } 45 | } 46 | abort!(Span::call_site(), "Expected wgsl! macro."); 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | # Run cargo test 14 | test: 15 | name: Test Suite 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 30 18 | steps: 19 | - name: Checkout sources 20 | uses: actions/checkout@v4 21 | - name: Cache 22 | uses: actions/cache@v3 23 | with: 24 | path: | 25 | ~/.cargo/bin/ 26 | ~/.cargo/registry/index/ 27 | ~/.cargo/registry/cache/ 28 | ~/.cargo/git/db/ 29 | target/ 30 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} 31 | - name: Install stable toolchain 32 | uses: dtolnay/rust-toolchain@stable 33 | - name: Install Dependencies 34 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 35 | - name: Run cargo clean 36 | run: cargo clean 37 | - name: Run cargo test 38 | run: cargo test 39 | 40 | # Run cargo clippy -- -D warnings 41 | clippy_check: 42 | name: Clippy 43 | runs-on: ubuntu-latest 44 | timeout-minutes: 30 45 | steps: 46 | - name: Checkout sources 47 | uses: actions/checkout@v4 48 | - name: Cache 49 | uses: actions/cache@v3 50 | with: 51 | path: | 52 | ~/.cargo/bin/ 53 | ~/.cargo/registry/index/ 54 | ~/.cargo/registry/cache/ 55 | ~/.cargo/git/db/ 56 | target/ 57 | key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.toml') }} 58 | - name: Install stable toolchain 59 | uses: dtolnay/rust-toolchain@stable 60 | with: 61 | components: clippy 62 | - name: Install Dependencies 63 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 64 | - name: Run clippy 65 | run: cargo clippy -- -D warnings 66 | -------------------------------------------------------------------------------- /src/pasting.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Delimiter, Span, TokenStream, TokenTree}; 2 | use proc_macro_error::abort; 3 | use quote::quote; 4 | 5 | pub fn wgsl_paste(stream: TokenStream) -> TokenStream { 6 | let mut iter = stream.into_iter(); 7 | let Some(TokenTree::Ident(definition)) = iter.next() else { 8 | abort!( 9 | Span::call_site(), 10 | "Expected `__wgsl_paste!($definition {to_be_pasted} [$($defined)*] $($tt)*)`!" 11 | ) 12 | }; 13 | let Some(TokenTree::Group(pasted)) = iter.next() else { 14 | abort!( 15 | Span::call_site(), 16 | "Expected `__wgsl_paste!($definition {to_be_pasted} [$($defined)*] $($tt)*)`!" 17 | ) 18 | }; 19 | let pasted = pasted.stream(); 20 | match iter.next() { 21 | // If some values are defined, check if this item has been defined. 22 | // If defined, skip, if not defined, paste and define this item. 23 | Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => { 24 | let tokens: TokenStream = iter.collect(); 25 | let mut found = false; 26 | let names: Vec<_> = g 27 | .stream() 28 | .into_iter() 29 | .filter(|x| match x { 30 | TokenTree::Ident(i) => { 31 | if i == &definition { 32 | found = true; 33 | } 34 | true 35 | } 36 | _ => false, 37 | }) 38 | .collect(); 39 | if found { 40 | quote!(::wgsl_ln::wgsl!([#(#names)*] #tokens)) 41 | } else { 42 | quote!(::wgsl_ln::wgsl!([#(#names)* #definition] #pasted #tokens)) 43 | } 44 | } 45 | // If no values defined, paste and define this item. 46 | other => { 47 | let tokens: TokenStream = iter.collect(); 48 | quote! { 49 | ::wgsl_ln::wgsl!([#definition] #pasted #other #tokens) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/wgsl_macro.rs: -------------------------------------------------------------------------------- 1 | use naga::valid::{Capabilities, ValidationFlags, Validator}; 2 | use proc_macro2::TokenStream; 3 | use proc_macro_error::abort; 4 | use quote::{format_ident, quote}; 5 | 6 | use crate::{sanitize::sanitize, to_wgsl_string::to_wgsl_string}; 7 | 8 | pub fn wgsl_macro(stream: TokenStream) -> TokenStream { 9 | let (stream, pastes) = sanitize(stream); 10 | if let Some(paste) = pastes { 11 | let paste = format_ident!("__wgsl_paste_{}", paste); 12 | return quote! {{use crate::*; #paste!(wgsl!(#stream))}}; 13 | } 14 | let mut spans = Vec::new(); 15 | let mut source = String::new(); 16 | #[allow(unused_variables)] 17 | let uses_naga_oil = to_wgsl_string(stream, &mut spans, &mut source); 18 | if uses_naga_oil { 19 | return quote! {#source}; 20 | } 21 | match naga::front::wgsl::parse_str(&source) { 22 | Ok(module) => { 23 | match Validator::new(ValidationFlags::all(), Capabilities::all()).validate(&module) { 24 | Ok(_) => quote! {#source}, 25 | Err(e) => { 26 | if let Some((span, _)) = e.spans().next() { 27 | let location = span.location(&source); 28 | let pos = match spans 29 | .binary_search_by_key(&(location.offset as usize), |x| x.0) 30 | { 31 | Ok(x) => x, 32 | Err(x) => x.saturating_sub(1), 33 | }; 34 | abort!(spans[pos].1, "Wgsl Error: {}", e) 35 | } 36 | let e_str = e.to_string(); 37 | quote! {compile_error!(#e_str)} 38 | } 39 | } 40 | } 41 | Err(e) => { 42 | if let Some((span, _)) = e.labels().next() { 43 | let location = span.location(&source); 44 | let pos = match spans.binary_search_by_key(&(location.offset as usize), |x| x.0) { 45 | Ok(x) => x, 46 | Err(x) => x.saturating_sub(1), 47 | }; 48 | abort!(spans[pos].1, "Wgsl Error: {}", e) 49 | } 50 | let e_str = e.to_string(); 51 | quote! {compile_error!(#e_str)} 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/sanitize.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | use proc_macro2::{ 3 | token_stream::IntoIter, Delimiter, Group, Ident, Spacing, TokenStream, TokenTree, 4 | }; 5 | /// Find the first instance of `$ident` and rewrite the macro as `__paste!(wgsl!())`. 6 | pub fn sanitize(stream: TokenStream) -> (TokenStream, Option) { 7 | let mut result = Vec::new(); 8 | let mut external_ident = false; 9 | let mut iter = stream.into_iter(); 10 | let mut first = true; 11 | while let Some(tt) = iter.next() { 12 | match tt { 13 | // ifndef 14 | TokenTree::Group(g) if first && g.delimiter() == Delimiter::Bracket => { 15 | result.push(TokenTree::Group(g)); 16 | } 17 | TokenTree::Punct(p) if p.as_char() == '$' => { 18 | external_ident = true; 19 | } 20 | // If $ident, import it and remove duplicated `$`s. 21 | TokenTree::Ident(ident) if external_ident => { 22 | result.push(TokenTree::Ident(ident.clone())); 23 | sanitize_remaining(iter, &ident, &mut result); 24 | return (TokenStream::from_iter(result), Some(ident)); 25 | } 26 | // Recursively look for `$`s. 27 | TokenTree::Group(g) => { 28 | external_ident = false; 29 | let delim = g.delimiter(); 30 | let (stream, ident) = sanitize(g.stream()); 31 | result.push(TokenTree::Group(Group::new(delim, stream))); 32 | if let Some(ident) = ident { 33 | sanitize_remaining(iter, &ident, &mut result); 34 | return (TokenStream::from_iter(result), Some(ident)); 35 | } 36 | } 37 | tt => { 38 | external_ident = false; 39 | result.push(tt) 40 | } 41 | } 42 | first = false 43 | } 44 | (TokenStream::from_iter(result), None) 45 | } 46 | 47 | /// Remove duplicated `$`s from `$ident`s. 48 | pub fn sanitize_remaining(stream: IntoIter, ident: &Ident, items: &mut Vec) { 49 | let mut last_is_symbol = false; 50 | for tt in stream { 51 | match &tt { 52 | // ifndef 53 | TokenTree::Punct(p) if p.as_char() == '$' => { 54 | last_is_symbol = true; 55 | items.push(tt) 56 | } 57 | TokenTree::Ident(i) if last_is_symbol && i == ident => { 58 | last_is_symbol = false; 59 | let _ = items.pop(); 60 | items.push(tt) 61 | } 62 | TokenTree::Group(g) => { 63 | last_is_symbol = false; 64 | let mut stream = Vec::new(); 65 | sanitize_remaining(g.stream().into_iter(), ident, &mut stream); 66 | items.push(TokenTree::Group(Group::new( 67 | g.delimiter(), 68 | TokenStream::from_iter(stream), 69 | ))) 70 | } 71 | _ => { 72 | last_is_symbol = false; 73 | items.push(tt) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wgsl_ln 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/wgsl_ln.svg)](https://crates.io/crates/wgsl_ln) 4 | [![Docs](https://docs.rs/wgsl_ln/badge.svg)](https://docs.rs/wgsl_ln/latest/wgsl_ln/) 5 | 6 | Experimental crate for writing wgsl in rust! 7 | 8 | ## The `wgsl!` macro 9 | 10 | The `wgsl!` macro converts normal rust tokens into a wgsl `&'static str`, similar to `stringify!`. 11 | This also validates the wgsl string using `naga`. Errors will be reported with 12 | the correct span. 13 | 14 | ```rust 15 | pub static MANHATTAN_DISTANCE: &str = wgsl!( 16 | fn manhattan_distance(a: vec2, b: vec2) -> f32 { 17 | return abs(a.x - b.x) + abs(a.y - b.y); 18 | } 19 | ); 20 | ``` 21 | 22 | Most errors can be caught at compile time. 23 | 24 | ```rust 25 | pub static MANHATTAN_DISTANCE: &str = wgsl!( 26 | fn manhattan_distance(a: vec2, b: vec2) -> f32 { 27 | // not allowed in wgsl 28 | abs(a.x - b.x) + abs(a.y - b.y) 29 | } 30 | ); 31 | ``` 32 | 33 | ## The `#[wgsl_export(name)]` macro 34 | 35 | Export a wgsl item (function, struct, etc) 36 | via `wgsl_export`. Must have the same `name` as the exported item. 37 | 38 | ```rust 39 | #[wgsl_export(manhattan_distance)] 40 | pub static MANHATTAN_DISTANCE: &str = wgsl!( 41 | fn manhattan_distance(a: vec2, b: vec2) -> f32 { 42 | return abs(a.x - b.x) + abs(a.y - b.y); 43 | } 44 | ); 45 | ``` 46 | 47 | ## Using an exported item 48 | 49 | ```rust 50 | pub static MANHATTAN_DISTANCE_TIMES_FIVE: &str = wgsl!( 51 | fn manhattan_distance_times_five(a: vec2, b: vec2) -> f32 { 52 | return $manhattan_distance(a, b) * 5.0; 53 | } 54 | ); 55 | ``` 56 | 57 | `#manhattan_distance` copies the `manhattan_distance` function into the module, 58 | making it usable. You can specify multiple instances of `#manhattan_distance` 59 | or omit the `#` in later usages. 60 | 61 | ## Ok what's actually going on? 62 | 63 | `wgsl_export` creates a `macro_rules!` macro that pastes itself into the `wgsl!` macro. 64 | The macro is `#[doc(hidden)]` and available in the crate root, 65 | i.e. `crate::__wgsl_paste_manhattan_distance!`. 66 | 67 | You don't need to import anything to use items defined in your crate, for other crates, 68 | you might want to blanket import the crate root. 69 | 70 | ```rust 71 | mod my_shaders { 72 | pub use external_shader_defs::*; 73 | 74 | pub static MAGIC: &str = wgsl!( 75 | fn magic() -> f32 { 76 | return #magic_number(); 77 | } 78 | ) 79 | } 80 | pub use my_shaders::MAGIC; 81 | ``` 82 | 83 | ## `naga_oil` support 84 | 85 | If a `#` is detected, we will disable certain validations. 86 | 87 | * Checks will be disabled when naga_oil preprocessor macros are detected. 88 | * All `#` starting statements has to end with either `;` or `}` to force a line break. 89 | 90 | ## License 91 | 92 | License under either of 93 | 94 | Apache License, Version 2.0 (LICENSE-APACHE or ) 95 | MIT license (LICENSE-MIT or ) 96 | at your option. 97 | 98 | ## Contribution 99 | 100 | Contributions are welcome! 101 | 102 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 103 | -------------------------------------------------------------------------------- /src/to_wgsl_string.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Delimiter, Spacing, Span, TokenStream, TokenTree}; 2 | 3 | use crate::open_close::{close, open}; 4 | 5 | trait StringMutExt { 6 | fn trim_space(&mut self); 7 | fn consume_prev(&mut self, c: char); 8 | } 9 | 10 | impl StringMutExt for String { 11 | fn trim_space(&mut self) { 12 | if self.ends_with(' ') { 13 | self.pop(); 14 | } 15 | } 16 | 17 | fn consume_prev(&mut self, c: char) { 18 | if matches!(c, ':' | ',' | '.' | ';') { 19 | self.trim_space(); 20 | } 21 | } 22 | } 23 | 24 | fn consume_post(c: char) -> bool { 25 | matches!(c, ':' | '.' | '@') 26 | } 27 | 28 | /// Convert to `wgsl` and return if we think this uses `naga_oil` or not. 29 | /// This has to format in a certain way to make `naga_oil` work: 30 | /// 31 | /// * Linebreaks after `;` and `}`. 32 | /// * Linebreaks before `#`. 33 | /// * No space after `#`. 34 | /// * No spaces before and after `:`. 35 | pub fn to_wgsl_string( 36 | stream: TokenStream, 37 | spans: &mut Vec<(usize, Span)>, 38 | string: &mut String, 39 | ) -> bool { 40 | let mut first = true; 41 | let mut uses_naga_oil = false; 42 | let mut iter = stream.into_iter().peekable(); 43 | while let Some(token) = iter.next() { 44 | match token { 45 | TokenTree::Group(g) if first && g.delimiter() == Delimiter::Bracket => (), 46 | TokenTree::Ident(i) => { 47 | spans.push((string.len(), i.span())); 48 | string.push_str(&i.to_string()); 49 | string.push(' '); 50 | } 51 | TokenTree::Punct(p) => { 52 | spans.push((string.len(), p.span())); 53 | string.consume_prev(p.as_char()); 54 | if p.as_char() == ';' { 55 | string.push(p.as_char()); 56 | string.push('\n'); 57 | } else if p.as_char() == '#' { 58 | uses_naga_oil = true; 59 | match iter.peek() { 60 | // Make sure `#{MATERIAL_BIND_GROUP}` stays in one line. 61 | Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => { 62 | string.push_str("#{"); 63 | to_wgsl_string(g.stream(), spans, string); 64 | iter.next(); 65 | string.trim_space(); 66 | string.push_str("} "); 67 | }, 68 | _ => { 69 | // new line and no spaces for naga_oil 70 | string.push('\n'); 71 | string.push(p.as_char()); 72 | } 73 | } 74 | } else if consume_post(p.as_char()) || p.spacing() == Spacing::Joint { 75 | string.push(p.as_char()); 76 | } else { 77 | string.push(p.as_char()); 78 | string.push(' '); 79 | } 80 | } 81 | TokenTree::Literal(l) => { 82 | spans.push((string.len(), l.span())); 83 | string.push_str(&l.to_string()); 84 | string.push(' '); 85 | } 86 | TokenTree::Group(g) => { 87 | if g.delimiter() == Delimiter::Bracket || g.delimiter() == Delimiter::Parenthesis { 88 | string.trim_space(); 89 | } 90 | spans.push((string.len(), g.delim_span().open())); 91 | string.push(open(g.delimiter())); 92 | if g.delimiter() == Delimiter::Brace { 93 | string.push('\n') 94 | } 95 | uses_naga_oil |= to_wgsl_string(g.stream(), spans, string); 96 | if string.ends_with(' ') { 97 | string.pop(); 98 | } 99 | spans.push((string.len(), g.delim_span().close())); 100 | string.push(close(g.delimiter())); 101 | if g.delimiter() == Delimiter::Brace { 102 | string.push('\n') 103 | } else { 104 | string.push(' '); 105 | } 106 | } 107 | } 108 | first = false; 109 | } 110 | uses_naga_oil 111 | } 112 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Experimental crate for writing wgsl in rust! 2 | //! 3 | //! # The `wgsl!` macro 4 | //! 5 | //! The `wgsl!` macro converts normal rust tokens into a wgsl `&'static str`, similar to [`stringify!`]. 6 | //! This also validates the wgsl string using [`naga`]. Errors will be reported with 7 | //! the correct span. 8 | //! 9 | //! ``` 10 | //! # use wgsl_ln::wgsl; 11 | //! pub static MANHATTAN_DISTANCE: &str = wgsl!( 12 | //! fn manhattan_distance(a: vec2, b: vec2) -> f32 { 13 | //! return abs(a.x - b.x) + abs(a.y - b.y); 14 | //! } 15 | //! ); 16 | //! ``` 17 | //! 18 | //! Most errors can be caught at compile time. 19 | //! 20 | //! ```compile_fail 21 | //! # use wgsl_ln::wgsl; 22 | //! pub static MANHATTAN_DISTANCE: &str = wgsl!( 23 | //! fn manhattan_distance(a: vec2, b: vec2) -> f32 { 24 | //! // not allowed in wgsl 25 | //! abs(a.x - b.x) + abs(a.y - b.y) 26 | //! } 27 | //! ); 28 | //! ``` 29 | //! 30 | //! # The `#[wgsl_export(name)]` macro 31 | //! 32 | //! Export a wgsl item (function, struct, etc) 33 | //! via `wgsl_export`. Must have the same `name` as the exported item. 34 | //! 35 | //! ``` 36 | //! # use wgsl_ln::{wgsl, wgsl_export}; 37 | //! #[wgsl_export(manhattan_distance)] 38 | //! pub static MANHATTAN_DISTANCE: &str = wgsl!( 39 | //! fn manhattan_distance(a: vec2, b: vec2) -> f32 { 40 | //! return abs(a.x - b.x) + abs(a.y - b.y); 41 | //! } 42 | //! ); 43 | //! ``` 44 | //! 45 | //! # Using an exported item with `$item` 46 | //! 47 | //! ``` 48 | //! # use wgsl_ln::{wgsl, wgsl_export}; 49 | //! # #[wgsl_export(manhattan_distance)] 50 | //! # pub static MANHATTAN_DISTANCE: &str = wgsl!( 51 | //! # fn manhattan_distance(a: vec2, b: vec2) -> f32 { 52 | //! # return abs(a.x - b.x) + abs(a.y - b.y); 53 | //! # } 54 | //! # ); 55 | //! pub static MANHATTAN_DISTANCE_TIMES_FIVE: &str = wgsl!( 56 | //! fn manhattan_distance_times_five(a: vec2, b: vec2) -> f32 { 57 | //! return $manhattan_distance(a, b) * 5.0; 58 | //! } 59 | //! ); 60 | //! ``` 61 | //! 62 | //! `#manhattan_distance` copies the `manhattan_distance` function into the module, 63 | //! making it usable. You can specify multiple instances of `#manhattan_distance` 64 | //! or omit the `#` in later usages. 65 | //! 66 | //! * Note compile time checks still work. 67 | //! 68 | //! ```compile_fail 69 | //! # use wgsl_ln::{wgsl, wgsl_export}; 70 | //! # #[wgsl_export(manhattan_distance)] 71 | //! # pub static MANHATTAN_DISTANCE: &str = wgsl!( 72 | //! # fn manhattan_distance(a: vec2, b: vec2) -> f32 { 73 | //! # return abs(a.x - b.x) + abs(a.y - b.y); 74 | //! # } 75 | //! # ); 76 | //! pub static MANHATTAN_DISTANCE_TIMES_FIVE: &str = wgsl!( 77 | //! fn manhattan_distance_times_five(a: vec2, b: vec2) -> f32 { 78 | //! // missing semicolon 79 | //! return $manhattan_distance(a, b) * 5.0 80 | //! } 81 | //! ); 82 | //! ``` 83 | //! 84 | //! # Ok what's actually going on? 85 | //! 86 | //! `wgsl_export` creates a `macro_rules!` macro that pastes itself into the `wgsl!` macro. 87 | //! The macro is `#[doc(hidden)]` and available in the crate root, 88 | //! i.e. `crate::__wgsl_paste_manhattan_distance!`. 89 | //! 90 | //! You don't need to import anything to use items defined in your crate, for other crates, 91 | //! you might want to blanket import the crate root. 92 | //! 93 | //! ``` 94 | //! # /* 95 | //! mod my_shaders { 96 | //! pub use external_shader_defs::*; 97 | //! 98 | //! pub static MAGIC: &str = wgsl!( 99 | //! fn magic() -> f32 { 100 | //! return $magic_number(); 101 | //! } 102 | //! ) 103 | //! } 104 | //! pub use my_shaders::MAGIC; 105 | //! # */ 106 | //! ``` 107 | //! 108 | //! # `naga_oil` support 109 | //! 110 | //! * If a `#` is detected, we will disable certain validations. 111 | //! * All `#` starting statements has to end with either `;` or `}` to force a line break. 112 | //! 113 | 114 | use proc_macro::TokenStream as TokenStream1; 115 | use proc_macro_error::{proc_macro_error, set_dummy}; 116 | use quote::quote; 117 | mod pasting; 118 | mod open_close; 119 | mod sanitize; 120 | mod to_wgsl_string; 121 | mod wgsl_macro; 122 | mod wgsl_export_macro; 123 | 124 | /// Converts normal rust tokens into a wgsl `&'static str`, similar to [`stringify!`]. 125 | /// This also validates the wgsl string using [`naga`]. Errors will be reported with 126 | /// the correct span. 127 | /// 128 | /// ``` 129 | /// # use wgsl_ln::wgsl; 130 | /// pub static MANHATTAN_DISTANCE: &str = wgsl!( 131 | /// fn manhattan_distance(a: vec2, b: vec2) -> f32 { 132 | /// return abs(a.x - b.x) + abs(a.y - b.y); 133 | /// } 134 | /// ); 135 | /// ``` 136 | /// 137 | /// To import an exported item, use the `$name` syntax. See crate level documentation for details. 138 | /// 139 | /// ``` 140 | /// # use wgsl_ln::{wgsl, wgsl_export}; 141 | /// # #[wgsl_export(manhattan_distance)] 142 | /// # pub static MANHATTAN_DISTANCE: &str = wgsl!( 143 | /// # fn manhattan_distance(a: vec2, b: vec2) -> f32 { 144 | /// # return abs(a.x - b.x) + abs(a.y - b.y); 145 | /// # } 146 | /// # ); 147 | /// pub static MANHATTAN_DISTANCE_SQUARED: &str = wgsl!( 148 | /// fn manhattan_distance_squared(a: vec2, b: vec2) -> f32 { 149 | /// // Using one `$` on the first item is also fine, we will deduplicate items. 150 | /// return $manhattan_distance(a, b) * $manhattan_distance(a, b); 151 | /// } 152 | /// ); 153 | /// ``` 154 | #[proc_macro] 155 | #[proc_macro_error] 156 | pub fn wgsl(stream: TokenStream1) -> TokenStream1 { 157 | set_dummy(quote! {""}); 158 | wgsl_macro::wgsl_macro(stream.into()).into() 159 | } 160 | 161 | /// Export a wgsl item (function, struct, etc). 162 | /// 163 | /// Must have the same `name` as the exported item. 164 | /// 165 | /// ``` 166 | /// # use wgsl_ln::{wgsl, wgsl_export}; 167 | /// #[wgsl_export(manhattan_distance)] 168 | /// pub static MANHATTAN_DISTANCE: &str = wgsl!( 169 | /// fn manhattan_distance(a: vec2, b: vec2) -> f32 { 170 | /// return abs(a.x - b.x) + abs(a.y - b.y); 171 | /// } 172 | /// ); 173 | /// ``` 174 | #[proc_macro_attribute] 175 | #[proc_macro_error] 176 | pub fn wgsl_export(attr: TokenStream1, stream: TokenStream1) -> TokenStream1 { 177 | set_dummy(quote! {""}); 178 | 179 | wgsl_export_macro::wgsl_export_macro(attr.into(), stream.into()).into() 180 | } 181 | 182 | /// Paste and avoid duplicates. 183 | #[doc(hidden)] 184 | #[proc_macro] 185 | #[proc_macro_error] 186 | pub fn __wgsl_paste(stream: TokenStream1) -> TokenStream1 { 187 | set_dummy(quote! {""}); 188 | 189 | pasting::wgsl_paste(stream.into()).into() 190 | } 191 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Mincong Lu 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. --------------------------------------------------------------------------------