diff --git a/framework/jinux-frame/src/arch/x86/cpu.rs b/framework/jinux-frame/src/arch/x86/cpu.rs index 6f57b1a65..61389ea2e 100644 --- a/framework/jinux-frame/src/arch/x86/cpu.rs +++ b/framework/jinux-frame/src/arch/x86/cpu.rs @@ -5,9 +5,11 @@ use core::fmt::Debug; use trapframe::{GeneralRegs, UserContext as RawUserContext}; +#[cfg(feature = "intel_tdx")] +use crate::arch::tdx_guest::{virtual_exception_handler, TdxTrapFrame}; use log::debug; #[cfg(feature = "intel_tdx")] -use tdx_guest::{serial_println, tdcall, tdvmcall, TdxVirtualExceptionType}; +use tdx_guest::tdcall; use x86_64::registers::rflags::RFlags; use crate::trap::call_irq_callback_functions; @@ -42,72 +44,52 @@ pub struct TrapInformation { } #[cfg(feature = "intel_tdx")] -pub fn virtual_exception_handler(trapframe: &mut GeneralRegs, ve_info: &tdcall::TdgVeInfo) { - match ve_info.exit_reason.into() { - TdxVirtualExceptionType::Hlt => { - serial_println!("Ready to halt"); - tdvmcall::hlt(); - } - TdxVirtualExceptionType::Io => { - if !handle_ve_io(trapframe, ve_info) { - serial_println!("Handle tdx ioexit errors, ready to halt"); - tdvmcall::hlt(); - } - } - TdxVirtualExceptionType::MsrRead => { - let msr = tdvmcall::rdmsr(trapframe.rcx as u32).unwrap(); - trapframe.rax = (msr as u32 & u32::MAX) as usize; - trapframe.rdx = ((msr >> 32) as u32 & u32::MAX) as usize; - } - TdxVirtualExceptionType::MsrWrite => { - let data = trapframe.rax as u64 | ((trapframe.rdx as u64) << 32); - tdvmcall::wrmsr(trapframe.rcx as u32, data).unwrap(); - } - TdxVirtualExceptionType::CpuId => { - let cpuid_info = tdvmcall::cpuid(trapframe.rax as u32, trapframe.rcx as u32).unwrap(); - let mask = 0xFFFF_FFFF_0000_0000_usize; - trapframe.rax = (trapframe.rax & mask) | cpuid_info.eax; - trapframe.rbx = (trapframe.rbx & mask) | cpuid_info.ebx; - trapframe.rcx = (trapframe.rcx & mask) | cpuid_info.ecx; - trapframe.rdx = (trapframe.rdx & mask) | cpuid_info.edx; - } - TdxVirtualExceptionType::Other => panic!("Unknown TDX vitrual exception type"), - _ => return, - } - trapframe.rip = trapframe.rip + ve_info.exit_instruction_length as usize; -} +struct VeGeneralRegs<'a>(&'a mut GeneralRegs); #[cfg(feature = "intel_tdx")] -pub fn handle_ve_io(trapframe: &mut GeneralRegs, ve_info: &tdcall::TdgVeInfo) -> bool { - let size = match ve_info.exit_qualification & 0x3 { - 0 => 1, - 1 => 2, - 3 => 4, - _ => panic!("Invalid size value"), - }; - let direction = if (ve_info.exit_qualification >> 3) & 0x1 == 0 { - tdvmcall::Direction::Out - } else { - tdvmcall::Direction::In - }; - let string = (ve_info.exit_qualification >> 4) & 0x1 == 1; - let repeat = (ve_info.exit_qualification >> 5) & 0x1 == 1; - let operand = if (ve_info.exit_qualification >> 6) & 0x1 == 0 { - tdvmcall::Operand::Dx - } else { - tdvmcall::Operand::Immediate - }; - let port = (ve_info.exit_qualification >> 16) as u16; - - match direction { - tdvmcall::Direction::In => { - trapframe.rax = tdvmcall::io_read(size, port).unwrap() as usize; - } - tdvmcall::Direction::Out => { - tdvmcall::io_write(size, port, trapframe.rax as u32).unwrap(); - } - }; - true +impl TdxTrapFrame for VeGeneralRegs<'_> { + fn rax(&self) -> usize { + self.0.rax + } + fn set_rax(&mut self, rax: usize) { + self.0.rax = rax; + } + fn rbx(&self) -> usize { + self.0.rbx + } + fn set_rbx(&mut self, rbx: usize) { + self.0.rbx = rbx; + } + fn rcx(&self) -> usize { + self.0.rcx + } + fn set_rcx(&mut self, rcx: usize) { + self.0.rcx = rcx; + } + fn rdx(&self) -> usize { + self.0.rdx + } + fn set_rdx(&mut self, rdx: usize) { + self.0.rdx = rdx; + } + fn rsi(&self) -> usize { + self.0.rsi + } + fn set_rsi(&mut self, rsi: usize) { + self.0.rsi = rsi; + } + fn rdi(&self) -> usize { + self.0.rdi + } + fn set_rdi(&mut self, rdi: usize) { + self.0.rdi = rdi; + } + fn rip(&self) -> usize { + self.0.rip + } + fn set_rip(&mut self, rip: usize) { + self.0.rip = rip; + } } impl UserContext { @@ -148,7 +130,8 @@ impl UserContextApiInternal for UserContext { if *exception == VIRTUALIZATION_EXCEPTION { let ve_info = tdcall::get_veinfo().expect("#VE handler: fail to get VE info\n"); - virtual_exception_handler(self.general_regs_mut(), &ve_info); + let mut ve_f = VeGeneralRegs(self.general_regs_mut()); + virtual_exception_handler(&mut ve_f, &ve_info); continue; } if exception.typ == CpuExceptionType::FaultOrTrap diff --git a/framework/jinux-frame/src/arch/x86/mod.rs b/framework/jinux-frame/src/arch/x86/mod.rs index a55be1644..852b796bb 100644 --- a/framework/jinux-frame/src/arch/x86/mod.rs +++ b/framework/jinux-frame/src/arch/x86/mod.rs @@ -6,6 +6,8 @@ pub(crate) mod irq; mod kernel; pub(crate) mod mm; pub(crate) mod pci; +#[cfg(feature = "intel_tdx")] +pub(crate) mod tdx_guest; pub(crate) mod timer; use alloc::fmt; diff --git a/framework/jinux-frame/src/arch/x86/tdx_guest.rs b/framework/jinux-frame/src/arch/x86/tdx_guest.rs new file mode 100644 index 000000000..9828a52e8 --- /dev/null +++ b/framework/jinux-frame/src/arch/x86/tdx_guest.rs @@ -0,0 +1,89 @@ +use tdx_guest::{ + tdcall::TdgVeInfo, + tdvmcall::{cpuid, hlt, rdmsr, wrmsr}, + {serial_println, tdcall, tdvmcall, TdxVirtualExceptionType}, +}; + +pub trait TdxTrapFrame { + fn rax(&self) -> usize; + fn set_rax(&mut self, rax: usize); + fn rbx(&self) -> usize; + fn set_rbx(&mut self, rbx: usize); + fn rcx(&self) -> usize; + fn set_rcx(&mut self, rcx: usize); + fn rdx(&self) -> usize; + fn set_rdx(&mut self, rdx: usize); + fn rsi(&self) -> usize; + fn set_rsi(&mut self, rsi: usize); + fn rdi(&self) -> usize; + fn set_rdi(&mut self, rdi: usize); + fn rip(&self) -> usize; + fn set_rip(&mut self, rip: usize); +} + +fn io_handler(trapframe: &mut dyn TdxTrapFrame, ve_info: &tdcall::TdgVeInfo) -> bool { + let size = match ve_info.exit_qualification & 0x3 { + 0 => 1, + 1 => 2, + 3 => 4, + _ => panic!("Invalid size value"), + }; + let direction = if (ve_info.exit_qualification >> 3) & 0x1 == 0 { + tdvmcall::Direction::Out + } else { + tdvmcall::Direction::In + }; + let string = (ve_info.exit_qualification >> 4) & 0x1 == 1; + let repeat = (ve_info.exit_qualification >> 5) & 0x1 == 1; + let operand = if (ve_info.exit_qualification >> 6) & 0x1 == 0 { + tdvmcall::Operand::Dx + } else { + tdvmcall::Operand::Immediate + }; + let port = (ve_info.exit_qualification >> 16) as u16; + + match direction { + tdvmcall::Direction::In => { + trapframe.set_rax(tdvmcall::io_read(size, port).unwrap() as usize); + } + tdvmcall::Direction::Out => { + tdvmcall::io_write(size, port, trapframe.rax() as u32).unwrap(); + } + }; + true +} + +pub fn virtual_exception_handler(trapframe: &mut impl TdxTrapFrame, ve_info: &TdgVeInfo) { + match ve_info.exit_reason.into() { + TdxVirtualExceptionType::Hlt => { + serial_println!("Ready to halt"); + hlt(); + } + TdxVirtualExceptionType::Io => { + if !io_handler(trapframe, ve_info) { + serial_println!("Handle tdx ioexit errors, ready to halt"); + hlt(); + } + } + TdxVirtualExceptionType::MsrRead => { + let msr = rdmsr(trapframe.rcx() as u32).unwrap(); + trapframe.set_rax((msr as u32 & u32::MAX) as usize); + trapframe.set_rdx(((msr >> 32) as u32 & u32::MAX) as usize); + } + TdxVirtualExceptionType::MsrWrite => { + let data = trapframe.rax() as u64 | ((trapframe.rdx() as u64) << 32); + wrmsr(trapframe.rcx() as u32, data).unwrap(); + } + TdxVirtualExceptionType::CpuId => { + let cpuid_info = cpuid(trapframe.rax() as u32, trapframe.rcx() as u32).unwrap(); + let mask = 0xFFFF_FFFF_0000_0000_usize; + trapframe.set_rax((trapframe.rax() & mask) | cpuid_info.eax); + trapframe.set_rbx((trapframe.rbx() & mask) | cpuid_info.ebx); + trapframe.set_rcx((trapframe.rcx() & mask) | cpuid_info.ecx); + trapframe.set_rdx((trapframe.rdx() & mask) | cpuid_info.edx); + } + TdxVirtualExceptionType::Other => panic!("Unknown TDX vitrual exception type"), + _ => return, + } + trapframe.set_rip(trapframe.rip() + ve_info.exit_instruction_length as usize); +} diff --git a/framework/jinux-frame/src/trap/handler.rs b/framework/jinux-frame/src/trap/handler.rs index 216e5d620..4c81eac6b 100644 --- a/framework/jinux-frame/src/trap/handler.rs +++ b/framework/jinux-frame/src/trap/handler.rs @@ -1,76 +1,58 @@ use crate::{arch::irq::IRQ_LIST, cpu::CpuException}; #[cfg(feature = "intel_tdx")] -use tdx_guest::{serial_println, tdcall, tdvmcall, TdxVirtualExceptionType}; +use crate::arch::tdx_guest::{virtual_exception_handler, TdxTrapFrame}; +#[cfg(feature = "intel_tdx")] +use tdx_guest::tdcall; use trapframe::TrapFrame; #[cfg(feature = "intel_tdx")] -pub fn virtual_exception_handler(trapframe: &mut TrapFrame, ve_info: &tdcall::TdgVeInfo) { - match ve_info.exit_reason.into() { - TdxVirtualExceptionType::Hlt => { - serial_println!("Ready to halt"); - tdvmcall::hlt(); - } - TdxVirtualExceptionType::Io => { - if !handle_ve_io(trapframe, ve_info) { - serial_println!("Handle tdx ioexit errors, ready to halt"); - tdvmcall::hlt(); - } - } - TdxVirtualExceptionType::MsrRead => { - let msr = tdvmcall::rdmsr(trapframe.rcx as u32).unwrap(); - trapframe.rax = (msr as u32 & u32::MAX) as usize; - trapframe.rdx = ((msr >> 32) as u32 & u32::MAX) as usize; - } - TdxVirtualExceptionType::MsrWrite => { - let data = trapframe.rax as u64 | ((trapframe.rdx as u64) << 32); - tdvmcall::wrmsr(trapframe.rcx as u32, data).unwrap(); - } - TdxVirtualExceptionType::CpuId => { - let cpuid_info = tdvmcall::cpuid(trapframe.rax as u32, trapframe.rcx as u32).unwrap(); - let mask = 0xFFFF_FFFF_0000_0000_usize; - trapframe.rax = (trapframe.rax & mask) | cpuid_info.eax; - trapframe.rbx = (trapframe.rbx & mask) | cpuid_info.ebx; - trapframe.rcx = (trapframe.rcx & mask) | cpuid_info.ecx; - trapframe.rdx = (trapframe.rdx & mask) | cpuid_info.edx; - } - TdxVirtualExceptionType::Other => panic!("Unknown TDX vitrual exception type"), - _ => return, - } - trapframe.rip = trapframe.rip + ve_info.exit_instruction_length as usize; -} +struct VeTrapFrame<'a>(&'a mut TrapFrame); #[cfg(feature = "intel_tdx")] -pub fn handle_ve_io(trapframe: &mut TrapFrame, ve_info: &tdcall::TdgVeInfo) -> bool { - let size = match ve_info.exit_qualification & 0x3 { - 0 => 1, - 1 => 2, - 3 => 4, - _ => panic!("Invalid size value"), - }; - let direction = if (ve_info.exit_qualification >> 3) & 0x1 == 0 { - tdvmcall::Direction::Out - } else { - tdvmcall::Direction::In - }; - let string = (ve_info.exit_qualification >> 4) & 0x1 == 1; - let repeat = (ve_info.exit_qualification >> 5) & 0x1 == 1; - let operand = if (ve_info.exit_qualification >> 6) & 0x1 == 0 { - tdvmcall::Operand::Dx - } else { - tdvmcall::Operand::Immediate - }; - let port = (ve_info.exit_qualification >> 16) as u16; - - match direction { - tdvmcall::Direction::In => { - trapframe.rax = tdvmcall::io_read(size, port).unwrap() as usize; - } - tdvmcall::Direction::Out => { - tdvmcall::io_write(size, port, trapframe.rax as u32).unwrap(); - } - }; - true +impl TdxTrapFrame for VeTrapFrame<'_> { + fn rax(&self) -> usize { + self.0.rax + } + fn set_rax(&mut self, rax: usize) { + self.0.rax = rax; + } + fn rbx(&self) -> usize { + self.0.rbx + } + fn set_rbx(&mut self, rbx: usize) { + self.0.rbx = rbx; + } + fn rcx(&self) -> usize { + self.0.rcx + } + fn set_rcx(&mut self, rcx: usize) { + self.0.rcx = rcx; + } + fn rdx(&self) -> usize { + self.0.rdx + } + fn set_rdx(&mut self, rdx: usize) { + self.0.rdx = rdx; + } + fn rsi(&self) -> usize { + self.0.rsi + } + fn set_rsi(&mut self, rsi: usize) { + self.0.rsi = rsi; + } + fn rdi(&self) -> usize { + self.0.rdi + } + fn set_rdi(&mut self, rdi: usize) { + self.0.rdi = rdi; + } + fn rip(&self) -> usize { + self.0.rip + } + fn set_rip(&mut self, rip: usize) { + self.0.rip = rip; + } } /// Only from kernel @@ -80,7 +62,8 @@ extern "sysv64" fn trap_handler(f: &mut TrapFrame) { #[cfg(feature = "intel_tdx")] if f.trap_num as u16 == 20 { let ve_info = tdcall::get_veinfo().expect("#VE handler: fail to get VE info\n"); - virtual_exception_handler(f, &ve_info); + let mut ve_f = VeTrapFrame(f); + virtual_exception_handler(&mut ve_f, &ve_info); } #[cfg(not(feature = "intel_tdx"))] panic!("cannot handle kernel cpu fault now, information:{:#x?}", f); diff --git a/framework/libs/tdx-guest/src/lib.rs b/framework/libs/tdx-guest/src/lib.rs index 56f6fbeb3..8ed1f0a1f 100644 --- a/framework/libs/tdx-guest/src/lib.rs +++ b/framework/libs/tdx-guest/src/lib.rs @@ -4,7 +4,7 @@ extern crate alloc; -pub mod asm; +mod asm; pub mod tdcall; pub mod tdvmcall; @@ -15,79 +15,23 @@ use raw_cpuid::{native_cpuid::cpuid_count, CpuIdResult}; use tdcall::{InitError, TdgVeInfo, TdgVpInfo}; use tdvmcall::*; -const TDX_CPUID_LEAF_ID: u64 = 0x21; - pub fn tdx_early_init() -> Result { - match is_tdx_guest() { - Ok(_) => Ok(tdcall::get_tdinfo()?), - Err(err) => Err(err), - } + check_tdx_guest()?; + Ok(tdcall::get_tdinfo()?) } -fn is_tdx_guest() -> Result<(), InitError> { +fn check_tdx_guest() -> Result<(), InitError> { + const TDX_CPUID_LEAF_ID: u64 = 0x21; let cpuid_leaf = cpuid_count(0, 0).eax as u64; if cpuid_leaf < TDX_CPUID_LEAF_ID { return Err(InitError::TdxCpuLeafIdError); } let cpuid_result: CpuIdResult = cpuid_count(TDX_CPUID_LEAF_ID as u32, 0); - if &cpuid_result.ebx.to_ne_bytes() == b"Inte" - && &cpuid_result.ebx.to_ne_bytes() == b"lTDX" - && &cpuid_result.ecx.to_ne_bytes() == b" " + if &cpuid_result.ebx.to_ne_bytes() != b"Inte" + || &cpuid_result.ebx.to_ne_bytes() != b"lTDX" + || &cpuid_result.ecx.to_ne_bytes() != b" " { - Ok(()) - } else { - Err(InitError::TdxVendorIdError) + return Err(InitError::TdxVendorIdError); } -} - -pub trait TdxTrapFrame { - fn rax(&self) -> usize; - fn set_rax(&mut self, rax: usize); - fn rbx(&self) -> usize; - fn set_rbx(&mut self, rbx: usize); - fn rcx(&self) -> usize; - fn set_rcx(&mut self, rcx: usize); - fn rdx(&self) -> usize; - fn set_rdx(&mut self, rdx: usize); - fn rsi(&self) -> usize; - fn set_rsi(&mut self, rsi: usize); - fn rdi(&self) -> usize; - fn set_rdi(&mut self, rdi: usize); - fn rip(&self) -> usize; - fn set_rip(&mut self, rip: usize); -} - -pub fn virtual_exception_handler(trapframe: &mut impl TdxTrapFrame, ve_info: &TdgVeInfo) { - match ve_info.exit_reason.into() { - TdxVirtualExceptionType::Hlt => { - serial_println!("Ready to halt"); - hlt(); - } - TdxVirtualExceptionType::Io => { - if !handle_io(trapframe, ve_info) { - serial_println!("Handle tdx ioexit errors, ready to halt"); - hlt(); - } - } - TdxVirtualExceptionType::MsrRead => { - let msr = rdmsr(trapframe.rcx() as u32).unwrap(); - trapframe.set_rax((msr as u32 & u32::MAX) as usize); - trapframe.set_rdx(((msr >> 32) as u32 & u32::MAX) as usize); - } - TdxVirtualExceptionType::MsrWrite => { - let data = trapframe.rax() as u64 | ((trapframe.rdx() as u64) << 32); - wrmsr(trapframe.rcx() as u32, data).unwrap(); - } - TdxVirtualExceptionType::CpuId => { - let cpuid_info = cpuid(trapframe.rax() as u32, trapframe.rcx() as u32).unwrap(); - let mask = 0xFFFF_FFFF_0000_0000_usize; - trapframe.set_rax((trapframe.rax() & mask) | cpuid_info.eax); - trapframe.set_rbx((trapframe.rbx() & mask) | cpuid_info.ebx); - trapframe.set_rcx((trapframe.rcx() & mask) | cpuid_info.ecx); - trapframe.set_rdx((trapframe.rdx() & mask) | cpuid_info.edx); - } - TdxVirtualExceptionType::Other => panic!("Unknown TDX vitrual exception type"), - _ => return, - } - trapframe.set_rip(trapframe.rip() + ve_info.exit_instruction_length as usize); + Ok(()) } diff --git a/framework/libs/tdx-guest/src/tdvmcall.rs b/framework/libs/tdx-guest/src/tdvmcall.rs index f961a5673..d85f810c9 100644 --- a/framework/libs/tdx-guest/src/tdvmcall.rs +++ b/framework/libs/tdx-guest/src/tdvmcall.rs @@ -5,7 +5,7 @@ //! resumes the TD via a SEAMCALL [TDH.VP.ENTER] invocation. extern crate alloc; -use crate::{asm::asm_td_vmcall, tdcall::TdgVeInfo, TdxTrapFrame}; +use crate::{asm::asm_td_vmcall, tdcall::TdgVeInfo}; use alloc::fmt; use bitflags::bitflags; use core::fmt::Write; @@ -193,38 +193,6 @@ pub fn write_mmio(size: u64, mmio_addr: u64, data: u64) -> Result<(), TdVmcallEr } } -pub fn handle_io(trapframe: &mut impl TdxTrapFrame, ve_info: &TdgVeInfo) -> bool { - let size = match ve_info.exit_qualification & 0x3 { - 0 => 1, - 1 => 2, - 3 => 4, - _ => panic!("Invalid size value"), - }; - let direction = if (ve_info.exit_qualification >> 3) & 0x1 == 0 { - Direction::Out - } else { - Direction::In - }; - let string = (ve_info.exit_qualification >> 4) & 0x1 == 1; - let repeat = (ve_info.exit_qualification >> 5) & 0x1 == 1; - let operand = if (ve_info.exit_qualification >> 6) & 0x1 == 0 { - Operand::Dx - } else { - Operand::Immediate - }; - let port = (ve_info.exit_qualification >> 16) as u16; - - match direction { - Direction::In => { - trapframe.set_rax(io_read(size, port).unwrap() as usize); - } - Direction::Out => { - io_write(size, port, trapframe.rax() as u32).unwrap(); - } - }; - true -} - macro_rules! io_read { ($port:expr, $ty:ty) => {{ let mut args = TdVmcallArgs { @@ -286,22 +254,42 @@ fn td_vmcall(args: &mut TdVmcallArgs) -> Result<(), TdVmcallError> { } bitflags! { - struct LineSts: u8 { - const INPUT_FULL = 1; - const OUTPUT_EMPTY = 1 << 5; - } + struct LineSts: u8 { + const INPUT_FULL = 1; + const OUTPUT_EMPTY = 1 << 5; + } } -fn line_sts() -> LineSts { +fn read_line_sts() -> LineSts { LineSts::from_bits_truncate(unsafe { PortRead::read_from_port(SERIAL_LINE_STS) }) } struct Serial; +impl Serial { + fn serial_write_byte(byte: u8) { + match byte { + // Backspace/Delete + 8 | 0x7F => { + while !read_line_sts().contains(LineSts::OUTPUT_EMPTY) {} + io_write!(SERIAL_IO_PORT, 8, u8).unwrap(); + while !read_line_sts().contains(LineSts::OUTPUT_EMPTY) {} + io_write!(SERIAL_IO_PORT, b' ', u8).unwrap(); + while !read_line_sts().contains(LineSts::OUTPUT_EMPTY) {} + io_write!(SERIAL_IO_PORT, 8, u8).unwrap(); + } + _ => { + while !read_line_sts().contains(LineSts::OUTPUT_EMPTY) {} + io_write!(SERIAL_IO_PORT, byte, u8).unwrap(); + } + } + } +} + impl Write for Serial { fn write_str(&mut self, s: &str) -> fmt::Result { for &c in s.as_bytes() { - serial_write_byte(c); + Serial::serial_write_byte(c); } Ok(()) } @@ -313,23 +301,6 @@ pub fn print(args: fmt::Arguments) { .expect("Failed to write to serial port"); } -fn serial_write_byte(byte: u8) { - match byte { - 8 | 0x7F => { - while !line_sts().contains(LineSts::OUTPUT_EMPTY) {} - io_write!(SERIAL_IO_PORT, 8, u8).unwrap(); - while !line_sts().contains(LineSts::OUTPUT_EMPTY) {} - io_write!(SERIAL_IO_PORT, b' ', u8).unwrap(); - while !line_sts().contains(LineSts::OUTPUT_EMPTY) {} - io_write!(SERIAL_IO_PORT, 8, u8).unwrap(); - } - _ => { - while !line_sts().contains(LineSts::OUTPUT_EMPTY) {} - io_write!(SERIAL_IO_PORT, byte, u8).unwrap(); - } - } -} - #[macro_export] macro_rules! serial_print { ($fmt: literal $(, $($arg: tt)+)?) => { @@ -339,7 +310,7 @@ macro_rules! serial_print { #[macro_export] macro_rules! serial_println { - ($fmt: literal $(, $($arg: tt)+)?) => { - $crate::tdvmcall::print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?)) - } + ($fmt: literal $(, $($arg: tt)+)?) => { + $crate::tdvmcall::print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?)) + } }