1#![deny(missing_docs)]
5extern crate proc_macro;
6
7use std::path::{Path, PathBuf};
8use std::process::Command;
9use std::{env, fs};
10
11use quote::{format_ident, quote};
12use syn::{
13 FnArg, GenericArgument, GenericParam, Generics, Ident, ItemFn, Lifetime, Pat, PathArguments,
14 Safety, Type, parse_macro_input,
15};
16use toml::Table;
17use toml::map::Entry;
18
19fn to_explicit_lifetimes(
21 lifetime: Option<&Lifetime>,
22 name: &Ident,
23 i: u32,
24 extra_lifetimes: &mut Vec<Lifetime>,
25) -> Option<Lifetime> {
26 if let Some(lifetime) = lifetime
27 && lifetime.ident != "_"
28 {
29 return None;
30 }
31
32 let ident = format_ident!("_gpu_kernel_lifetime_{}_{}", name, i);
34 let lifetime = Lifetime::new(&format!("'{}", ident), ident.span());
35 extra_lifetimes.push(lifetime.clone());
36 Some(lifetime)
37}
38
39fn type_with_explicit_lifetimes(
42 ty: &Type,
43 name: &Ident,
44 i: u32,
45 extra_lifetimes: &mut Vec<Lifetime>,
46) -> proc_macro2::TokenStream {
47 if let Type::Reference(r) = ty {
48 let and = &r.and_token;
49 let lifetime = to_explicit_lifetimes(r.lifetime.as_ref(), name, i, extra_lifetimes)
50 .or_else(|| r.lifetime.clone());
51 let elem = type_with_explicit_lifetimes(&r.elem, name, i, extra_lifetimes);
52 quote! { #and #lifetime #elem }
53 } else if let Type::Path(p) = ty {
54 let mut p = p.clone();
55 if let PathArguments::AngleBracketed(args) = &mut p
56 .path
57 .segments
58 .last_mut()
59 .expect("Unexpected type without last path segment")
60 .arguments
61 {
62 for a in args.args.iter_mut() {
63 if let GenericArgument::Lifetime(lifetime) = a
64 && let Some(l) = to_explicit_lifetimes(Some(lifetime), name, i, extra_lifetimes)
65 {
66 *lifetime = l;
67 }
68 }
69 }
70 quote! { #p }
71 } else {
72 quote! { #ty }
73 }
74}
75
76#[proc_macro_attribute]
117pub fn kernel(
118 _attr: proc_macro::TokenStream,
119 input: proc_macro::TokenStream,
120) -> proc_macro::TokenStream {
121 let func = parse_macro_input!(input as ItemFn);
122 let attrs = func.attrs;
123 let vis = func.vis;
124 let code = func.block;
125 let safety = func.sig.safety;
126 let is_unsafe = matches!(safety, Safety::Unsafe(_));
127 let orig_ident = func.sig.ident;
128 let kernel_ident = format_ident!("{}_gpu_kernel", orig_ident);
129 let kernel_struct_ident = format_ident!("GpuKernel_{}", orig_ident);
130 let inputs = func.sig.inputs;
131 let generics = func.sig.generics;
132 let where_clause = &generics.where_clause;
133 let output = func.sig.output;
134
135 assert!(
136 func.sig.asyncness.is_none(),
137 "#[kernel] `{orig_ident}` cannot be async",
138 );
139 for g in &generics.params {
141 if !matches!(g, GenericParam::Lifetime(_)) {
142 panic!("#[kernel] `{orig_ident}` cannot be generic");
143 }
144 }
145 assert!(
146 func.sig.variadic.is_none(),
147 "#[kernel] `{orig_ident}` cannot be variadic"
148 );
149
150 let mut input_tys = Vec::new();
152 let mut input_names = Vec::new();
154 let mut input_alignment_names = Vec::new();
157 let mut input_size_names = Vec::new();
158
159 let mut extra_lifetimes = Vec::new();
160
161 for (i, arg) in inputs.iter().enumerate() {
162 let mut name = format_ident!("_gpu_kernel_arg{i}");
163
164 match arg {
165 FnArg::Receiver(_) => {
166 panic!("#[kernel] `{orig_ident}` cannot have a `self` argument");
167 }
168 FnArg::Typed(arg) => {
169 assert!(
170 arg.attrs.is_empty(),
171 "#[kernel] `{orig_ident}` arg `{name}` cannot have attributes"
172 );
173 assert!(
174 !matches!(*arg.ty, Type::ImplTrait(_)),
175 "#[kernel] `{orig_ident}` arg `{name}` cannot be of `impl Trait` type"
176 );
177 if let Pat::Ident(ident) = &*arg.pat {
178 name = ident.ident.clone();
179 }
180 if let Type::Reference(r) = &*arg.ty {
181 assert!(
182 r.mutability.is_none(),
183 "#[kernel] `{orig_ident}` arg `{name}` cannot be a mutable reference"
184 );
185 assert!(
186 !matches!(*r.elem, Type::ImplTrait(_)),
187 "#[kernel] `{orig_ident}` arg `{name}` cannot be of `impl Trait` type"
188 );
189 }
190 if is_unsafe {
191 let ty = &arg.ty;
192 input_tys.push(quote! { #ty });
193 } else {
194 let ty = type_with_explicit_lifetimes(&arg.ty, &name, 0, &mut extra_lifetimes);
195 input_tys.push(quote! { impl ::gpu_kernel::SafeKernelArg<Output = #ty> });
196 }
197 }
198 }
199 input_alignment_names.push(format_ident!("_gpu_kernel_align_{name}"));
200 input_size_names.push(format_ident!("_gpu_kernel_size_{name}"));
201 input_names.push(name);
202 }
203
204 let cpu_generics = if generics.lt_token.is_some() {
205 if extra_lifetimes.is_empty() {
206 quote! { #generics }
207 } else {
208 let Generics {
209 lt_token,
210 params,
211 gt_token,
212 ..
213 } = &generics;
214 quote! { #lt_token #(#extra_lifetimes),*, #params #gt_token }
215 }
216 } else {
217 quote! { <#(#extra_lifetimes),*> }
218 };
219
220 let require_safe = if is_unsafe {
221 quote!()
222 } else {
223 quote!(
225 #(
226 let mut #input_names = <_ as ::gpu_kernel::SafeKernelArg>::into_kernel_arg(#input_names, &gpu_kernel_launch_config);
227 )*
228 )
229 };
230
231 let safe_attrs = if is_unsafe {
232 quote!()
233 } else {
234 quote! { #[allow(improper_ctypes_definitions, improper_gpu_kernel_arg)] }
236 };
237
238 let args;
240 let drop;
241 if input_names.len() == 1 {
242 args = quote! {
244 let mut _gpu_kernel_arg = #(#input_names)*;
246 let _gpu_kernel_args = &mut _gpu_kernel_arg;
247 };
248 drop = quote! {};
249 } else {
250 args = quote! {
254 let mut _gpu_kernel_size: usize = 0;
255 #(
256 let #input_alignment_names = std::mem::align_of_val(&#input_names);
257 #[allow(clippy::size_of_ref)]
258 let #input_size_names = std::mem::size_of_val(&#input_names);
259 _gpu_kernel_size =
260 _gpu_kernel_size.next_multiple_of(#input_alignment_names)
261 + #input_size_names;
262 )*
263
264 let mut _gpu_kernel_args = std::vec::Vec::<std::mem::MaybeUninit<u8>>::new();
265 _gpu_kernel_args.resize(_gpu_kernel_size, std::mem::MaybeUninit::uninit());
266
267 let mut _gpu_kernel_offset: usize = 0;
268 #(
269 _gpu_kernel_offset = _gpu_kernel_offset.next_multiple_of(#input_alignment_names);
271
272 unsafe {
274 std::ptr::write(_gpu_kernel_args.as_mut_ptr().add(_gpu_kernel_offset) as *mut _, #input_names);
275 }
276
277 _gpu_kernel_offset += #input_size_names;
278 )*
279
280 let _gpu_kernel_args = _gpu_kernel_args.as_mut_slice();
281 };
282
283 drop = quote! {
284 _gpu_kernel_offset = 0;
285 #(
286 _gpu_kernel_offset = _gpu_kernel_offset.next_multiple_of(#input_alignment_names);
288
289 unsafe {
292 #input_names = std::ptr::read(_gpu_kernel_args.as_ptr().add(_gpu_kernel_offset) as *const _);
293 }
294
295 _gpu_kernel_offset += #input_size_names;
296 )*
297 };
298 }
299
300 let output = quote! {
301 #[cfg(any(target_arch = "amdgpu", target_arch = "nvptx64"))]
304 #[allow(unused_imports)]
305 use ::gpu_kernel::prelude::*;
306
307 #[cfg(any(target_arch = "amdgpu", target_arch = "nvptx64"))]
309 #[unsafe(no_mangle)]
310 #(#attrs)*
311 #safe_attrs
312 #vis #safety extern "gpu-kernel" fn #kernel_ident #generics(#inputs) #where_clause #output
313 #code
314
315 #[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
318 #[allow(non_camel_case_types)]
319 #vis struct #kernel_struct_ident(::gpu_kernel::Kernel);
320
321 #[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
322 #[allow(non_upper_case_globals)]
323 #(#attrs)*
324 #vis static #orig_ident: std::sync::LazyLock<#kernel_struct_ident> = std::sync::LazyLock::new(|| {
325 #kernel_struct_ident(crate::KERNEL_LIB_CALLED_IN_CRATE.get_kernel(std::stringify!(#kernel_ident)))
326 });
327
328 #[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
329 impl std::ops::Deref for #kernel_struct_ident {
330 type Target = ::gpu_kernel::Kernel;
331
332 fn deref(&self) -> &Self::Target {
333 &self.0
334 }
335 }
336
337 #[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
338 impl #kernel_struct_ident {
339 #vis #safety fn launch #cpu_generics(&self, gpu_kernel_launch_config: &::gpu_kernel::LaunchConfig, #(mut #input_names: #input_tys),*) #where_clause {
340 #require_safe
341 #args
342 unsafe {
344 self.launch_impl(gpu_kernel_launch_config, _gpu_kernel_args);
345 }
346 #drop
347 }
348 }
349 };
350
351 proc_macro::TokenStream::from(output)
352}
353
354#[proc_macro]
358pub fn kernel_lib_impl_dbg(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
359 kernel_lib_impl(tokens, true)
360}
361
362#[proc_macro]
366pub fn kernel_lib_impl_rel(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
367 kernel_lib_impl(tokens, false)
368}
369
370fn get_rustflags(env_rustflags: &str, manifest_dir: &Path, target: &str) -> String {
372 let mut all_rustflags = env_rustflags.to_string();
373 for path in manifest_dir
374 .ancestors()
375 .map(|p| p.join(".cargo"))
376 .chain(std::iter::once(
377 env::var("CARGO_HOME")
378 .map(PathBuf::from)
379 .unwrap_or_else(|_| env::home_dir().expect("$CARGO_HOME or ~ must be set")),
380 ))
381 {
382 let cargo_config_path = path.join("config.toml");
383 let config_rustflags =
384 if fs::exists(&cargo_config_path).expect("Failed to check for .cargo/config.toml") {
385 let config = fs::read_to_string(&cargo_config_path)
386 .expect("Failed to read .cargo/config.toml");
387 let config = config
388 .parse::<Table>()
389 .expect("Invalid toml in .cargo/config.toml");
390 config
391 .get("target")
392 .and_then(|v| {
393 v.as_table()
394 .expect("Failed to parse .cargo/config.toml")
395 .get(target)
396 })
397 .and_then(|v| {
398 v.as_table()
399 .expect("Failed to parse .cargo/config.toml")
400 .get("rustflags")
401 })
402 .map(|v| {
403 v.as_array()
404 .expect("Failed to parse .cargo/config.toml")
405 .iter()
406 .map(|v| {
407 v.as_str()
408 .expect("Failed to parse .cargo/config.toml")
409 .to_string()
410 })
411 .collect::<Vec<_>>()
412 })
413 .unwrap_or_default()
414 } else {
415 Vec::new()
416 };
417 let mut new_rustflags = config_rustflags.join(" ");
419 new_rustflags.push_str(&all_rustflags);
420 all_rustflags = new_rustflags;
421 }
422 all_rustflags
423}
424
425struct NewCargoToml {
426 cargo_toml: String,
427 has_gpu_feature: bool,
428}
429
430fn create_cargo_toml(
435 manifest_path: &Path,
436 manifest_dir: &Path,
437 gpu_toml_dir: &Path,
438 orig: &str,
439) -> NewCargoToml {
440 let mut cargo_toml = orig
441 .parse::<Table>()
442 .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", manifest_path.display()));
443 let has_gpu_feature = cargo_toml
444 .get("features")
445 .map(|v| {
446 v.as_table()
447 .expect("features needs to be a toml table")
448 .contains_key("gpu")
449 })
450 .unwrap_or_default();
451 let has_lib = cargo_toml.contains_key("lib")
452 || fs::exists(manifest_dir.join("src").join("lib.rs")).expect("Failed to check for lib.rs");
453 let lib_config = cargo_toml
454 .entry("lib")
455 .or_insert_with(|| Table::new().into())
456 .as_table_mut()
457 .expect("lib needs to be a toml table");
458
459 let rel_prefix = {
461 let manifest = &manifest_dir; let gpu = gpu_toml_dir
463 .canonicalize()
464 .expect("Failed to resolve $CARGO_TARGET_DIR");
465 if let Ok(rel) = gpu.strip_prefix(manifest) {
466 let diff = rel.components().count();
467 vec![".."; diff].join("/")
468 } else {
469 manifest.display().to_string()
471 }
472 };
473
474 match lib_config.entry("path") {
476 Entry::Vacant(e) => {
477 let path = if has_lib {
478 format!("{rel_prefix}/src/lib.rs")
479 } else {
480 format!("{rel_prefix}/src/main.rs")
481 };
482 e.insert(path.into());
483 }
484 Entry::Occupied(mut e) => {
485 let path = Path::new(e.get().as_str().expect("lib path must be a toml string"));
487 if path.is_relative() {
488 let new = Path::new(&rel_prefix).join(path).display().to_string();
489 e.insert(new.into());
490 }
491 }
492 }
493
494 lib_config.insert("crate-type".into(), vec!["cdylib"].into());
495
496 let fix_dep = |v: &mut toml::Value| {
498 if let Some(v) = v.as_table_mut()
499 && let Some(p) = v.get_mut("path")
500 {
501 let path = Path::new(p.as_str().expect("Dependency path must be a toml string"));
502 if path.is_relative() {
503 let new = Path::new(&rel_prefix).join(path).display().to_string();
504 *p = new.into();
505 }
506 }
507 };
508 let dep_keys = &["dependencies", "build-dependencies", "dev-dependencies"];
509 let fix_all_deps = |t: &mut Table| {
510 for k in dep_keys {
511 if let Some(t) = t.get_mut(*k) {
512 let t = t
513 .as_table_mut()
514 .unwrap_or_else(|| panic!("{k} must be a toml table"));
515 for (_, v) in t.iter_mut() {
516 fix_dep(v);
517 }
518 }
519 }
520 };
521
522 fix_all_deps(&mut cargo_toml);
524 if let Some(t) = cargo_toml.get_mut("target") {
525 let t = t.as_table_mut().expect("target must be a toml table");
526 for (_, v) in t.iter_mut() {
527 fix_all_deps(v.as_table_mut().expect("target must contain toml tables"));
528 }
529 }
530 NewCargoToml {
531 cargo_toml: cargo_toml.to_string(),
532 has_gpu_feature,
533 }
534}
535
536fn kernel_lib_impl(_: proc_macro::TokenStream, debug: bool) -> proc_macro::TokenStream {
537 #[cfg(feature = "amd")]
538 let target = "amdgcn-amd-amdhsa";
539 #[cfg(not(feature = "amd"))]
540 let target = "";
541
542 let target_env = target.replace('-', "_").to_uppercase();
543 let target_rustflags = format!("CARGO_TARGET_{target_env}_RUSTFLAGS");
544 let target_cargoflags = format!("CARGO_TARGET_{target_env}_FLAGS");
545
546 let crate_name = env::var("CARGO_CRATE_NAME").expect("$CARGO_CRATE_NAME must be set");
548 let kernel_file = format!("{crate_name}.elf");
549 let manifest_dir =
550 PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR must be set"))
551 .canonicalize()
552 .expect("Failed to resolve $CARGO_MANIFEST_DIR");
553 let manifest_path =
554 PathBuf::from(env::var("CARGO_MANIFEST_PATH").expect("$CARGO_MANIFEST_PATH must be set"));
555 let lock_path = manifest_dir.join("Cargo.lock");
556 let target_dir = env::var("CARGO_TARGET_DIR")
558 .map(PathBuf::from)
559 .unwrap_or_else(|_| manifest_dir.join("target"))
560 .join("gpu-kernel");
561 let kernel_path = target_dir
562 .join(target)
563 .join(if debug { "debug" } else { "release" })
564 .join(&kernel_file);
565
566 let env_rustflags = env::var(&target_rustflags).unwrap_or_default();
567 let cargoflags = env::var(&target_cargoflags).unwrap_or_default();
569 let cargoflags = cargoflags.trim();
570
571 let all_rustflags = get_rustflags(&env_rustflags, &manifest_dir, target);
572
573 let target_cpu = {
575 let i = all_rustflags.rfind("target-cpu").unwrap_or_else(|| panic!("Did not find target-cpu, make sure to set `-Ctarget-cpu=...` in ${target_rustflags}"));
576 let start = i + "target-cpu".len() + 1;
577 let end = all_rustflags[start..]
578 .find(' ')
579 .map(|i| start + i)
580 .unwrap_or(all_rustflags.len());
581 &all_rustflags[start..end]
582 };
583 #[cfg(feature = "amd")]
585 let is_wave64_enabled = all_rustflags
586 .rfind("+wavefrontsize64")
587 .map(|i| {
588 if let Some(j) = all_rustflags.rfind("-wavefrontsize64") {
589 i > j
590 } else {
591 true
592 }
593 })
594 .unwrap_or_default();
595
596 #[cfg(feature = "amd")]
597 let link_args =
598 amdgpu_device_libs_build::get_link_args(is_wave64_enabled, &target_cpu).link_args;
599 #[cfg(not(feature = "amd"))]
600 let link_args = [target_cpu];
601 let new_rustflags = link_args
602 .iter()
603 .map(|v| format!("-Clink-arg={v}"))
604 .collect::<Vec<_>>();
605
606 let cargo_toml = fs::read_to_string(&manifest_path)
608 .unwrap_or_else(|e| panic!("Failed to read {}: {e}", manifest_path.display()));
609
610 let gpu_toml_dir = target_dir.clone();
611 fs::create_dir_all(&gpu_toml_dir).expect("Failed to create gpu-kernel target dir");
612 let NewCargoToml {
613 cargo_toml,
614 has_gpu_feature,
615 } = create_cargo_toml(&manifest_path, &manifest_dir, &gpu_toml_dir, &cargo_toml);
616
617 let gpu_toml = gpu_toml_dir.join("Cargo.toml");
619 fs::write(&gpu_toml, cargo_toml.as_bytes()).expect("Failed to write GPU Cargo.toml");
620 if let Err(e) = fs::copy(&lock_path, gpu_toml_dir.join("Cargo.lock")) {
622 println!("Warning: Failed to copy Cargo.lock to GPU directory ({e}), ignoring");
623 }
624
625 let mut cargo = Command::new("cargo");
626 cargo.args([
627 "build",
628 "--target",
629 target,
630 "--lib",
631 "-Zbuild-std=core,alloc",
632 "-m",
633 &gpu_toml.display().to_string(),
634 "--target-dir",
635 &target_dir.display().to_string(),
636 ]);
637 if has_gpu_feature {
638 cargo.arg("--features=gpu");
639 }
640 if !debug {
641 cargo.args([
646 "--release",
647 "-Zpanic-immediate-abort",
648 "--config=profile.release.panic=\"immediate-abort\"",
649 ]);
650 } else {
651 cargo.arg("--config=profile.dev.opt-level=2");
654 }
655 if !cargoflags.is_empty() {
656 for f in cargoflags.split(' ') {
657 cargo.arg(f);
658 }
659 }
660
661 cargo.env(
662 &target_rustflags,
663 format!(
664 "{env_rustflags} {} -Clinker-plugin-lto",
665 new_rustflags.join(" ")
666 ),
667 );
668 let res = cargo
669 .status()
670 .expect("Failed to run cargo to compile for GPU");
671 if !res.success() {
672 panic!("Cargo did not exit successfully, failed to compile for GPU");
673 }
674
675 let kernel_path = kernel_path.display().to_string();
676 let manifest_path = manifest_path.display().to_string();
677 let lock_path = lock_path.display().to_string();
678 let output = quote! {
679 const _: &[u8] = std::include_bytes!(#manifest_path);
683 const _: &[u8] = std::include_bytes!(#lock_path);
684 const _: std::option::Option<&str> = std::option_env!("CARGO_TARGET_DIR");
685 const _: std::option::Option<&str> = std::option_env!(#target_rustflags);
686 const _: std::option::Option<&str> = std::option_env!(#target_cargoflags);
687
688 #[doc(hidden)]
689 static GPU_KERNEL_MODULE_DATA: &[u8] = std::include_bytes!(#kernel_path);
690 #[doc(hidden)]
694 static KERNEL_LIB_CALLED_IN_CRATE: std::sync::LazyLock<::gpu_kernel::Module> = std::sync::LazyLock::new(|| ::gpu_kernel::Module::new(GPU_KERNEL_MODULE_DATA));
695 };
696 proc_macro::TokenStream::from(output)
697}