1use proc_macro::TokenStream;
16use proc_macro2::Ident;
17use quote::{format_ident, quote};
18use std::{
19 collections::BTreeSet,
20 path::{Path, PathBuf},
21};
22use syn::{
23 parse::{Parse, ParseStream},
24 parse_macro_input,
25 spanned::Spanned,
26 Error, Fields, Item, ItemEnum, ItemStruct, LitStr,
27};
28use turbo_genesis_abi::{
29 TurboProgramChannelMetadata, TurboProgramCommandMetadata, TurboProgramMetadata,
30};
31
32fn get_project_path() -> PathBuf {
40 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
41 assert!(
42 !manifest_dir.is_empty(),
43 "CARGO_MANIFEST_DIR unavailable. Could not determine project directory."
44 );
45 Path::new(&manifest_dir).to_path_buf()
46}
47
48fn create_program_metadata(program_name: &str) -> TurboProgramMetadata {
57 use base64::{engine::general_purpose::URL_SAFE_NO_PAD as b64_url_safe, Engine};
58 use sha2::{Digest, Sha256};
59
60 let project_dir = get_project_path();
62 let cargo_toml_path = project_dir.join("Cargo.toml");
63 let cargo_toml = std::fs::read_to_string(&cargo_toml_path)
64 .unwrap_or_else(|e| panic!("Could not read Cargo.toml: {e:?}"));
65
66 let parsed: toml_edit::DocumentMut = cargo_toml
68 .parse()
69 .unwrap_or_else(|e| panic!("Invalid Cargo.toml syntax: {e:?}"));
70 let user_id = parsed["package"]["metadata"]["turbo"]["user"]
71 .as_str()
72 .expect("Missing [package.metadata.turbo].user entry");
73
74 let uuid = uuid::Uuid::parse_str(user_id).expect("Invalid UUID format");
76 let mut hasher = Sha256::new();
77 hasher.update(uuid.as_bytes());
78 hasher.update(program_name.as_bytes());
79 let program_id = b64_url_safe.encode(hasher.finalize());
80
81 TurboProgramMetadata {
82 name: program_name.to_string(),
83 program_id,
84 owner_id: user_id.to_string(),
85 commands: BTreeSet::new(),
86 channels: BTreeSet::new(),
87 }
88}
89
90#[proc_macro_attribute]
106pub fn serialize(_attr: TokenStream, item: TokenStream) -> TokenStream {
107 let input = parse_macro_input!(item as Item);
109
110 match &input {
112 Item::Struct(_) | Item::Enum(_) => (),
113 _ => {
114 return quote! {
116 compile_error!("#[turbo::serialize] only supports structs and enums.");
117 }
118 .into();
119 }
120 };
121
122 let expanded = quote! {
124 #[derive(
125 Debug,
126 Clone,
127 turbo::borsh::BorshDeserialize,
128 turbo::borsh::BorshSerialize,
129 turbo::serde::Deserialize,
130 turbo::serde::Serialize,
131 )]
132 #[borsh(crate = "turbo::borsh")]
133 #[serde(crate = "turbo::serde")]
134 #input
135 };
136
137 TokenStream::from(expanded)
138}
139
140#[proc_macro_attribute]
157pub fn game(_attr: TokenStream, item: TokenStream) -> TokenStream {
158 let item = parse_macro_input!(item as Item);
159 let (ident, default_state) = match &item {
160 Item::Struct(ItemStruct { ident, fields, .. }) => {
161 if *fields == Fields::Unit {
162 (ident, quote! { #ident })
163 } else if fields.is_empty() {
164 (ident, quote! { #ident {} })
165 } else {
166 (ident, quote! { #ident::new() })
167 }
168 }
169 Item::Enum(ItemEnum { ident, .. }) => (ident, quote! { #ident::new() }),
170 _ => {
171 return quote! {
172 compile_error!("#[turbo::game] only supports structs and enums.");
173 }
174 .into();
175 }
176 };
177
178 quote! {
179 #[turbo::serialize]
180 #item
181
182 #[no_mangle]
183 #[cfg(all(turbo_hot_reload, not(turbo_no_run)))]
184 pub unsafe extern "C" fn run() {
185 use turbo::borsh::*;
186 let mut state = match hot::load() {
187 Ok(bytes) => <#ident>::try_from_slice(&bytes).unwrap_or_else(|_| #default_state),
188 Err(err) => {
189 log!("[turbo] Hot reload deserialization failed: {err:?}");
190 #default_state
191 },
192 };
193 state.update();
194 if let Err(err) = turbo::lifecycle::on_update() {
195 turbo::log!("turbo::on_update error: {err:?}");
196 }
197 if let Ok(bytes) = borsh::to_vec(&state) {
198 if let Err(err) = hot::save(&bytes) {
199 log!("[turbo] hot save failed: Error code {err}");
200 }
201 }
202 }
203
204 #[no_mangle]
205 #[cfg(all(turbo_hot_reload, not(turbo_no_run)))]
206 pub unsafe extern "C" fn on_before_hot_reload() {
207 if let Err(err) = turbo::lifecycle::on_before_hot_reload() {
208 turbo::log!("turbo::on_before_hot_reload error: {err:?}");
209 }
210 }
211
212 #[no_mangle]
213 #[cfg(all(turbo_hot_reload, not(turbo_no_run)))]
214 pub unsafe extern "C" fn on_after_hot_reload() {
215 if let Err(err) = turbo::lifecycle::on_after_hot_reload() {
216 turbo::log!("turbo::on_after_hot_reload error: {err:?}");
217 }
218 }
219
220 #[no_mangle]
221 #[cfg(all(turbo_hot_reload, not(turbo_no_run)))]
222 pub unsafe extern "C" fn on_reset() {
223 if let Err(err) = turbo::lifecycle::on_reset() {
224 turbo::log!("turbo::on_reset error: {err:?}");
225 }
226 }
227
228 #[no_mangle]
229 #[cfg(all(not(turbo_hot_reload), not(turbo_no_run)))]
230 pub unsafe extern "C" fn run() {
231 static mut GAME_STATE: Option<#ident> = None;
232 let mut state = GAME_STATE.take().unwrap_or_else(|| #default_state);
233 state.update();
234 if let Err(err) = turbo::lifecycle::on_update() {
235 turbo::log!("turbo::on_update error: {err:?}");
236 }
237 GAME_STATE = Some(state);
238 }
239
240 #[doc(hidden)]
241 #[cfg(turbo_no_run)]
242 pub(crate) mod __turbo_os_program_utils {
243 pub fn log_input_as_json<T: turbo::borsh::BorshDeserialize + turbo::serde::Serialize>() {
245 use turbo::borsh::BorshDeserialize;
246 use turbo::serde_json::json;
247 let bytes = turbo::os::server::command::read_input();
248 if bytes.is_empty() {
249 return turbo::log!("null");
250 }
251 let data = match T::try_from_slice(&bytes) {
252 Ok(data) => data,
253 Err(err) => return turbo::log!("{}", json!({ "error": err.to_string(), "input": bytes })),
254 };
255 let json = json!(data);
256 turbo::log!("{}", json)
257 }
258 }
259 }.into()
260}
261
262#[derive(Debug, Clone, Default)]
268struct CommandArgs {
269 pub program: String,
271 pub name: String,
273}
274impl Parse for CommandArgs {
275 fn parse(input: ParseStream) -> syn::Result<Self> {
276 let mut args = Self::default();
277 while !input.is_empty() {
278 let ident: syn::Ident = input.parse()?;
279 let _: syn::Token![=] = input.parse()?;
280 let value: LitStr = input.parse()?;
281 match ident.to_string().as_str() {
282 "program" => args.program = value.value(),
283 "name" => args.name = value.value(),
284 _ => return Err(syn::Error::new_spanned(ident, "unexpected attribute key")),
285 }
286 if input.peek(syn::Token![,]) {
287 let _: syn::Token![,] = input.parse()?;
288 }
289 }
290 if args.program.is_empty() {
291 return Err(input.error("missing `program`"));
292 }
293 if args.name.is_empty() {
294 return Err(input.error("missing `name`"));
295 }
296 Ok(args)
297 }
298}
299
300#[proc_macro_attribute]
316pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
317 let item = parse_macro_input!(item as Item);
319
320 let args = parse_macro_input!(attr as CommandArgs);
322
323 match &item {
325 Item::Struct(ItemStruct { ident, .. }) | Item::Enum(ItemEnum { ident, .. }) => {
326 let mut program_metadata = create_program_metadata(&args.program);
328 process_program_command(&mut program_metadata, args.clone(), &item, ident)
330 }
331 _ => Error::new(
333 item.span(),
334 "`#[command]` may only be used on a struct or enum that implements `CommandHandler`",
335 )
336 .to_compile_error()
337 .into(),
338 }
339}
340
341fn process_program_command(
364 program_metadata: &mut TurboProgramMetadata,
365 args: CommandArgs,
366 item: &Item,
367 ident: &Ident,
368) -> TokenStream {
369 let program_id = program_metadata.program_id.as_str();
371 let program_name = program_metadata.name.as_str();
372 let owner_id = program_metadata.owner_id.as_str();
373 let name = args.name;
374
375 program_metadata
377 .commands
378 .insert(TurboProgramCommandMetadata { name: name.clone() });
379
380 let handler_export = format!("turbo_program:command_handler/{}/{}", program_id, name);
382 let handler_extern = format_ident!("command_handler_{}_{}", program_name, name);
383 let de_input_export = format!("turbo_program:de_command_input/{}/{}", program_id, name);
384 let de_input_extern = format_ident!("de_command_input_{}_{}", program_name, name);
385
386 let metadata_string = serde_json::to_string(program_metadata).unwrap() + "\n";
388 let metadata_bytes = metadata_string.as_bytes().iter().map(|b| quote! { #b });
389 let metadata_len = metadata_bytes.len();
390 let metadata_ident = format_ident!(
391 "turbo_os_program_metadata_command_{}_{}",
392 program_name,
393 name
394 );
395
396 quote! {
398 #[used]
400 #[doc(hidden)]
401 #[allow(non_upper_case_globals)]
402 #[link_section = "turbo_os_program_metadata"]
403 pub static #metadata_ident: [u8; #metadata_len] = [#(#metadata_bytes),*];
404
405 #[turbo::serialize]
407 #item
408
409 impl #ident {
411 pub const PROGRAM_ID: &'static str = #program_id;
413 pub const PROGRAM_OWNER: &'static str = #owner_id;
415
416 pub fn exec(self) -> String {
418 turbo::os::client::command::exec(#program_id, #name, self)
419 }
420 }
421
422 #[cfg(turbo_no_run)]
424 #[unsafe(export_name = #handler_export)]
425 pub unsafe extern "C" fn #handler_extern() -> usize {
426 let user_id = turbo::os::server::command::user_id();
427 let mut cmd = turbo::os::server::command::parse_input::<#ident>();
428 match &mut cmd {
429 Ok(cmd) => match cmd.run(&user_id) {
430 Ok(_) => turbo::os::server::command::COMMIT,
431 Err(_) => turbo::os::server::command::CANCEL,
432 },
433 Err(_) => turbo::os::server::command::CANCEL,
434 }
435 }
436
437 #[cfg(turbo_no_run)]
439 #[unsafe(export_name = #de_input_export)]
440 pub unsafe extern "C" fn #de_input_extern() {
441 crate::__turbo_os_program_utils::log_input_as_json::<#ident>()
442 }
443 }
444 .into()
445}
446
447#[derive(Debug, Clone, Default)]
453struct ChannelArgs {
454 pub program: String,
456 pub name: String,
458}
459impl Parse for ChannelArgs {
460 fn parse(input: ParseStream) -> syn::Result<Self> {
461 let mut args = Self::default();
462 while !input.is_empty() {
463 let ident: syn::Ident = input.parse()?;
464 let _: syn::Token![=] = input.parse()?;
465 let value: LitStr = input.parse()?;
466 match ident.to_string().as_str() {
467 "program" => args.program = value.value(),
468 "name" => args.name = value.value(),
469 _ => return Err(syn::Error::new_spanned(ident, "unexpected attribute key")),
470 }
471 if input.peek(syn::Token![,]) {
472 let _: syn::Token![,] = input.parse()?;
473 }
474 }
475 if args.program.is_empty() {
476 return Err(input.error("missing `program`"));
477 }
478 if args.name.is_empty() {
479 return Err(input.error("missing `name`"));
480 }
481 Ok(args)
482 }
483}
484
485#[proc_macro_attribute]
503pub fn channel(attr: TokenStream, item: TokenStream) -> TokenStream {
504 let item = parse_macro_input!(item as Item);
506
507 let args = parse_macro_input!(attr as ChannelArgs);
509
510 match &item {
512 Item::Struct(ItemStruct { ident, .. }) | Item::Enum(ItemEnum { ident, .. }) => {
513 let mut program_metadata = create_program_metadata(&args.program);
515 let expanded =
517 process_program_channel(&mut program_metadata, args.clone(), &item, ident);
518 return expanded;
520 }
521 _ => Error::new(
523 item.span(),
524 "`#[channel]` may only be used on a struct or enum that implements `ChannelHandler`",
525 )
526 .to_compile_error()
527 .into(),
528 }
529}
530
531fn process_program_channel(
553 program_metadata: &mut TurboProgramMetadata,
554 args: ChannelArgs,
555 item: &Item,
556 ident: &Ident,
557) -> TokenStream {
558 let program_id = program_metadata.program_id.as_str();
560 let program_name = program_metadata.name.as_str();
561 let name = args.name;
562
563 program_metadata
565 .channels
566 .insert(TurboProgramChannelMetadata { name: name.clone() });
567
568 let handler_export = format!("turbo_program:channel_handler/{}/{}", program_id, name);
570 let handler_extern = format_ident!("channel_handler_{}_{}", program_name, name);
571 let de_send_export = format!("turbo_program:de_channel_send/{}/{}", program_id, name);
572 let de_send_extern = format_ident!("de_channel_send_{}_{}", program_name, name);
573 let de_recv_export = format!("turbo_program:de_channel_recv/{}/{}", program_id, name);
574 let de_recv_extern = format_ident!("de_channel_recv_{}_{}", program_name, name);
575
576 let metadata_string = serde_json::to_string(program_metadata).unwrap() + "\n";
578 let metadata_bytes = metadata_string.as_bytes().iter().map(|b| quote! { #b });
579 let metadata_len = metadata_bytes.len();
580 let metadata_ident = format_ident!(
581 "turbo_os_program_metadata_channel_{}_{}",
582 program_name,
583 name
584 );
585
586 quote! {
588 #[used]
590 #[doc(hidden)]
591 #[allow(non_upper_case_globals)]
592 #[link_section = "turbo_os_program_metadata"]
593 pub static #metadata_ident: [u8; #metadata_len] = [#(#metadata_bytes),*];
594
595 #[turbo::serialize]
597 #item
598
599 impl #ident {
601 pub fn subscribe(
603 channel_id: &str
604 ) -> Option<turbo::os::client::channel::ChannelConnection<
605 <Self as turbo::os::server::channel::ChannelHandler>::Recv,
606 <Self as turbo::os::server::channel::ChannelHandler>::Send,
607 >> {
608 turbo::os::client::channel::Channel::subscribe(#program_id, #name, channel_id)
609 }
610 }
611
612 #[cfg(turbo_no_run)]
614 #[unsafe(export_name = #handler_export)]
615 pub unsafe extern "C" fn #handler_extern() {
616 use turbo::os::server::channel::{ChannelSettings, ChannelMessage, ChannelError, recv_with_timeout};
617 let handler = &mut #ident::new();
618 let settings = &mut ChannelSettings::default();
619
620 if let Err(err) = handler.on_open(settings) {
622 turbo::log!("Error in on_open: {err:?}");
623 return;
624 }
625
626 let timeout = settings.interval.unwrap_or(u32::MAX).max(16);
628 loop {
629 match recv_with_timeout(timeout) {
630 Ok(ChannelMessage::Connect(user_id, _)) => {
631 let _ = handler.on_connect(&user_id).map_err(|e| turbo::log!("on_connect err: {e:?}"));
632 }
633 Ok(ChannelMessage::Disconnect(user_id, _)) => {
634 let _ = handler.on_disconnect(&user_id).map_err(|e| turbo::log!("on_disconnect err: {e:?}"));
635 }
636 Ok(ChannelMessage::Data(user_id, data)) => match #ident::parse(&data) {
637 Ok(data) => {
638 let _ = handler.on_data(&user_id, data).map_err(|e| turbo::log!("on_data err: {e:?}"));
639 }
640 Err(err) => turbo::log!("Error parsing data: {err:?}"),
641 }
642 Err(ChannelError::Timeout) => {
643 let _ = handler.on_interval().map_err(|e| turbo::log!("on_interval err: {e:?}"));
644 }
645 Err(_) => {
646 let _ = handler.on_close().map_err(|e| turbo::log!("on_close err: {e:?}"));
647 return;
648 }
649 }
650 }
651 }
652
653 #[cfg(turbo_no_run)]
655 #[unsafe(export_name = #de_send_export)]
656 pub unsafe extern "C" fn #de_send_extern() {
657 crate::__turbo_os_program_utils::log_input_as_json::<<#ident as turbo::os::server::channel::ChannelHandler>::Send>()
658 }
659
660 #[cfg(turbo_no_run)]
662 #[unsafe(export_name = #de_recv_export)]
663 pub unsafe extern "C" fn #de_recv_extern() {
664 crate::__turbo_os_program_utils::log_input_as_json::<<#ident as turbo::os::server::channel::ChannelHandler>::Recv>()
665 }
666 }.into()
667}
668
669#[derive(Debug, Clone, Default)]
675struct DocumentArgs {
676 pub program: String,
678}
679impl Parse for DocumentArgs {
680 fn parse(input: ParseStream) -> syn::Result<Self> {
681 let mut args = Self::default();
682 while !input.is_empty() {
683 let ident: syn::Ident = input.parse()?;
684 let _: syn::Token![=] = input.parse()?;
685 let value: LitStr = input.parse()?;
686 match ident.to_string().as_str() {
687 "program" => args.program = value.value(),
688 _ => return Err(syn::Error::new_spanned(ident, "unexpected attribute key")),
689 }
690 if input.peek(syn::Token![,]) {
691 let _: syn::Token![,] = input.parse()?;
692 }
693 }
694 if args.program.is_empty() {
695 return Err(input.error("missing `program`"));
696 }
697 Ok(args)
698 }
699}
700
701#[proc_macro_attribute]
724pub fn document(attr: TokenStream, item: TokenStream) -> TokenStream {
725 let item = parse_macro_input!(item as Item);
727
728 let args = parse_macro_input!(attr as DocumentArgs);
730
731 match &item {
733 Item::Struct(ItemStruct { ident, .. }) | Item::Enum(ItemEnum { ident, .. }) => {
734 let program_metadata = create_program_metadata(&args.program);
736 let program_id = program_metadata.program_id;
738 quote! {
742 #[turbo::serialize]
743 #item
744
745 impl turbo::os::HasProgramId for #ident {
746 const PROGRAM_ID: &'static str = #program_id;
747 }
748 }
749 .into()
750 }
751 _ => Error::new(
753 item.span(),
754 "`#[document]` may only be used on a struct or enum representing a document type",
755 )
756 .to_compile_error()
757 .into(),
758 }
759}