| // Copyright 2025 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| use std::fs; |
| use std::fs::File; |
| use std::io::Write; |
| use std::path::PathBuf; |
| |
| use anyhow::{Context, Result, anyhow}; |
| use clap::{Args, Parser, Subcommand}; |
| use minijinja::{Environment, State}; |
| use serde::Serialize; |
| use serde::de::DeserializeOwned; |
| |
| pub mod mpu_validation; |
| pub mod system_config; |
| pub mod validation; |
| |
| use mpu_validation::MpuValidationMode; |
| use mpu_validation::pmsav7::validate_pmsav7_layout; |
| use system_config::ObjectConfig::Interrupt; |
| use system_config::{ |
| InterruptTableConfig, MemoryMapping, MemoryMappingType, ObjectConfig, ProcessObjectConfig, |
| SystemConfig, |
| }; |
| |
| #[derive(Debug, Parser)] |
| pub struct Cli { |
| #[command(flatten)] |
| common_args: CommonArgs, |
| #[command(subcommand)] |
| command: Command, |
| } |
| |
| #[derive(Args, Debug)] |
| pub struct CommonArgs { |
| #[arg(long, required(true))] |
| config: PathBuf, |
| #[arg(long, required(true))] |
| output: PathBuf, |
| #[arg( |
| long("template"), |
| value_name = "NAME=PATH", |
| value_parser = parse_template, |
| action = clap::ArgAction::Append |
| )] |
| templates: Vec<(String, PathBuf)>, |
| #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] |
| userspace: bool, |
| } |
| |
| fn parse_template(s: &str) -> Result<(String, PathBuf), String> { |
| s.split_once('=') |
| .map(|(name, path)| (name.to_string(), path.into())) |
| .ok_or_else(|| format!("invalid template format: '{s}'. Expected NAME=PATH.")) |
| } |
| |
| #[derive(Subcommand, Debug)] |
| enum Command { |
| RenderTargetTemplate, |
| RenderAppTemplate(AppArgs), |
| } |
| |
| #[derive(Args, Debug)] |
| #[group(required = true, multiple = false)] |
| pub struct AppArgs { |
| #[arg(long)] |
| pub app_name: Option<String>, |
| |
| #[arg(long)] |
| pub process_name: Option<String>, |
| } |
| |
| enum AppType { |
| SingleProcess { app_name: String }, |
| MultiProcess { process_name: String }, |
| } |
| |
| impl From<&AppArgs> for AppType { |
| fn from(args: &AppArgs) -> AppType { |
| if let Some(ref app_name) = args.app_name { |
| return AppType::SingleProcess { |
| app_name: app_name.clone(), |
| }; |
| } |
| if let Some(ref process_name) = args.process_name { |
| return AppType::MultiProcess { |
| process_name: process_name.clone(), |
| }; |
| } |
| unreachable!("AppArgs group requires either app_name or process_name"); |
| } |
| } |
| |
| /// Context defining the specific app and process being passed to the app template for rendering. |
| #[derive(Serialize)] |
| pub struct AppRenderContext<'a, A: ArchConfigInterface + Serialize> { |
| pub arch: &'a A, |
| pub app: &'a system_config::AppConfig, |
| pub process: &'a system_config::ProcessConfig, |
| pub is_multi_process_app: bool, |
| } |
| |
| pub trait ArchConfigInterface { |
| fn get_arch_crate_name(&self) -> &'static str; |
| fn get_start_fn_address(&self, flash_start_address: u64) -> u64; |
| fn calculate_and_validate_config( |
| &mut self, |
| config: &mut system_config::BaseConfig, |
| ) -> Result<()>; |
| fn get_interrupt_table_link_section(&self) -> Option<String>; |
| |
| /// Returns true if the interrupt table entries should have no arguments. |
| /// |
| /// Vectored interrupt controllers (like NVIC on ARM) require the entries to |
| /// have no arguments because the hardware jumps directly to the handler. |
| /// Non-vectored controllers (or architectures where all interrupts go to a |
| /// single handler like RISC-V) need to capture the `from_userspace` flag |
| /// earlier in the call chain and pass it to the specific interrupt handler. |
| fn bare_interrupt_table_entries(&self) -> bool; |
| |
| /// Validate memory layout for MPU compatibility. |
| fn validate_mpu(&self, _config: &system_config::BaseConfig) -> Result<()> { |
| Ok(()) // Default: no MPU validation |
| } |
| /// Get required alignment for a process RAM region. |
| fn get_ram_alignment(&self, _size: u64) -> u64 { |
| 32 |
| } |
| /// Get minimum RAM alignment. |
| fn get_minimum_ram_alignment(&self) -> u64 { |
| 32 |
| } |
| } |
| |
| pub fn parse_config<A: ArchConfigInterface + DeserializeOwned>( |
| cli: &Cli, |
| ) -> Result<system_config::SystemConfig<A>> { |
| let json5_str = |
| fs::read_to_string(&cli.common_args.config).context("Failed to read config file")?; |
| let config: SystemConfig<A> = |
| serde_json5::from_str(&json5_str).context("Failed to parse config file")?; |
| Ok(config) |
| } |
| |
| const FLASH_ALIGNMENT: u64 = 4; |
| |
| /// Map the kernel's flash region as `ReadOnlyExecutable` into every app's |
| /// address space. This is required so that `svc_return` remains executable |
| /// after the privilege drop on Cortex-M. |
| /// |
| /// TODO: https://pwbug.dev/465500606 - Isolate `svc_return` into its own |
| /// section to allow mapping only that instead of the whole kernel flash. |
| fn cortex_m_add_kernel_code_mapping(config: &mut system_config::BaseConfig) { |
| for app in &mut config.apps { |
| for process in &mut app.processes { |
| process.memory_mappings.insert( |
| 0, |
| MemoryMapping { |
| name: "kernel_code".to_string(), |
| ty: MemoryMappingType::ReadOnlyExecutable, |
| start_address: config.kernel.flash_start_address, |
| size_bytes: config.kernel.flash_size_bytes, |
| }, |
| ); |
| } |
| } |
| } |
| |
| impl ArchConfigInterface for system_config::Armv8MConfig { |
| fn get_arch_crate_name(&self) -> &'static str { |
| "arch_arm_cortex_m" |
| } |
| |
| fn get_start_fn_address(&self, flash_start_address: u64) -> u64 { |
| // +1 to set the Thumb mode bit in the reset vector. |
| flash_start_address + 1 |
| } |
| |
| fn calculate_and_validate_config( |
| &mut self, |
| config: &mut system_config::BaseConfig, |
| ) -> Result<()> { |
| cortex_m_add_kernel_code_mapping(config); |
| |
| // PMSAv8 requires all region base addresses and sizes to be aligned to |
| // 32 bytes (the minimum MPU region granularity). |
| for app in &config.apps { |
| for process in &app.processes { |
| for mapping in &process.memory_mappings { |
| if mapping.start_address % 32 != 0 { |
| return Err(anyhow!( |
| "Unaligned memory mapping: process {} in application {}'s memory mapping {}'s start address ({:#10x}) must be aligned to 32 bytes", |
| process.name, |
| app.name, |
| mapping.name, |
| mapping.start_address, |
| )); |
| } |
| if mapping.size_bytes % 32 != 0 { |
| return Err(anyhow!( |
| "Unaligned memory mapping: process {} in application {}'s memory mapping {}'s size ({}) must be aligned to 32 bytes", |
| process.name, |
| app.name, |
| mapping.name, |
| mapping.size_bytes, |
| )); |
| } |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn get_interrupt_table_link_section(&self) -> Option<String> { |
| Some(".vector_table.interrupts".to_string()) |
| } |
| |
| fn bare_interrupt_table_entries(&self) -> bool { |
| true |
| } |
| } |
| |
| impl ArchConfigInterface for system_config::Armv7MConfig { |
| fn get_arch_crate_name(&self) -> &'static str { |
| "arch_arm_cortex_m" |
| } |
| |
| fn get_start_fn_address(&self, flash_start_address: u64) -> u64 { |
| // +1 to set the Thumb mode bit in the reset vector. |
| flash_start_address + 1 |
| } |
| |
| fn calculate_and_validate_config( |
| &mut self, |
| config: &mut system_config::BaseConfig, |
| ) -> Result<()> { |
| cortex_m_add_kernel_code_mapping(config); |
| |
| // Validate power-of-2 size for Armv7-M |
| for app in &config.apps { |
| for process in &app.processes { |
| if !process.ram_size_bytes.is_power_of_two() { |
| return Err(anyhow!( |
| "Process {} RAM size ({}) must be a power of 2 for Armv7-M", |
| process.name, |
| process.ram_size_bytes |
| )); |
| } |
| if process.ram_size_bytes < 32 { |
| return Err(anyhow!( |
| "Process {} RAM size ({}) must be at least 32 bytes for Armv7-M", |
| process.name, |
| process.ram_size_bytes |
| )); |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn get_ram_alignment(&self, size: u64) -> u64 { |
| size |
| } |
| |
| fn get_interrupt_table_link_section(&self) -> Option<String> { |
| Some(".vector_table.interrupts".to_string()) |
| } |
| |
| fn bare_interrupt_table_entries(&self) -> bool { |
| true |
| } |
| |
| fn validate_mpu(&self, config: &system_config::BaseConfig) -> Result<()> { |
| let kernel = &config.kernel; |
| |
| // Build app list: (name, flash_start, flash_end, ram_start, ram_end) |
| let apps: Vec<_> = config |
| .apps |
| .iter() |
| .map(|app| { |
| ( |
| app.name.clone(), |
| app.flash_start_address, |
| app.flash_start_address + app.flash_size_bytes, |
| app.ram_start_address, |
| app.ram_start_address + app.ram_size_bytes, |
| ) |
| }) |
| .collect(); |
| |
| let issues = validate_pmsav7_layout( |
| kernel.flash_start_address, |
| kernel.flash_start_address + kernel.flash_size_bytes, |
| kernel.ram_start_address, |
| kernel.ram_start_address + kernel.ram_size_bytes, |
| &apps, |
| ); |
| |
| if issues.is_empty() { |
| return Ok(()); |
| } |
| |
| // Report issues based on validation mode |
| let mode = kernel.mpu_validation; |
| let has_errors = issues.iter().any(|i| i.is_error); |
| |
| for issue in &issues { |
| match mode { |
| MpuValidationMode::Strict => { |
| eprintln!("error: {}", issue); |
| } |
| MpuValidationMode::Warn => { |
| if issue.is_error { |
| eprintln!("warning: {}", issue); |
| } else { |
| eprintln!("info: {}", issue); |
| } |
| } |
| MpuValidationMode::Permissive => { |
| // Silent - no output in permissive mode |
| } |
| } |
| } |
| |
| if mode == MpuValidationMode::Strict && has_errors { |
| return Err(anyhow!( |
| "PMSAv7 MPU validation failed. Use mpu_validation: \"warn\" to continue anyway." |
| )); |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl ArchConfigInterface for system_config::RiscVConfig { |
| fn get_arch_crate_name(&self) -> &'static str { |
| "arch_riscv" |
| } |
| |
| fn get_start_fn_address(&self, flash_start_address: u64) -> u64 { |
| flash_start_address |
| } |
| |
| fn calculate_and_validate_config( |
| &mut self, |
| _config: &mut system_config::BaseConfig, |
| ) -> Result<()> { |
| Ok(()) |
| } |
| |
| fn get_interrupt_table_link_section(&self) -> Option<String> { |
| None |
| } |
| |
| fn bare_interrupt_table_entries(&self) -> bool { |
| false |
| } |
| } |
| |
| pub struct SystemGenerator<'a, A: ArchConfigInterface> { |
| cli: Cli, |
| config: system_config::SystemConfig<A>, |
| env: Environment<'a>, |
| validators: Vec<Box<dyn crate::validation::ManifestValidator<A>>>, |
| } |
| |
| impl<'a, A: ArchConfigInterface + Serialize> SystemGenerator<'a, A> { |
| pub fn new(cli: Cli, config: system_config::SystemConfig<A>) -> Result<Self> { |
| let mut instance = Self { |
| cli, |
| config, |
| env: Environment::new(), |
| validators: vec![Box::new(crate::validation::IdentifierValidator)], |
| }; |
| instance.env.set_lstrip_blocks(true); |
| instance.env.set_trim_blocks(true); |
| |
| // Run manifest validation first before rendering any templates. |
| for validator in &instance.validators { |
| validator.validate(&instance.config)?; |
| } |
| |
| instance.env.add_filter("hex", hex); |
| instance.env.add_filter("to_lower_ident", to_lower_ident); |
| instance.env.add_filter("to_upper_ident", to_upper_ident); |
| for (name, path) in instance.cli.common_args.templates.clone() { |
| let template = fs::read_to_string(path)?; |
| instance.env.add_template_owned(name, template)?; |
| } |
| |
| instance.populate_addresses(); |
| instance.populate_process_and_thread_objects()?; |
| // This must be called after populate_addresses. |
| instance.populate_memory_mappings(); |
| instance.populate_interrupt_table()?; |
| |
| instance.config.base.userspace = instance.cli.common_args.userspace; |
| |
| // Calculate and validate config after the populations above. |
| instance.config.calculate_and_validate()?; |
| |
| // Run architecture-specific MPU compatibility validation. |
| instance.config.arch.validate_mpu(&instance.config.base)?; |
| |
| Ok(instance) |
| } |
| |
| pub fn generate(&mut self) -> Result<()> { |
| let out_str = match &self.cli.command { |
| Command::RenderTargetTemplate => self.render_system()?, |
| Command::RenderAppTemplate(args) => self.render_app_template(args)?, |
| }; |
| |
| let mut file = File::create(&self.cli.common_args.output)?; |
| file.write_all(out_str.as_bytes()) |
| .context("Failed to write output") |
| } |
| |
| fn render_system(&self) -> Result<String> { |
| let template = self.env.get_template("system")?; |
| template |
| .render(&self.config) |
| .context("Could not render system template") |
| } |
| |
| fn render_app_template(&self, args: &AppArgs) -> Result<String> { |
| let template = self.env.get_template("app")?; |
| let is_multi_process_app = args.process_name.is_some(); |
| |
| let (app, process) = match AppType::from(args) { |
| AppType::MultiProcess { process_name } => self |
| .config |
| .base |
| .get_process_and_app(&process_name) |
| .ok_or_else(|| { |
| anyhow!( |
| "Unable to find process `{}` in system manifest", |
| process_name |
| ) |
| })?, |
| AppType::SingleProcess { app_name } => { |
| let a = self.config.base.get_app(&app_name).ok_or_else(|| { |
| anyhow!("Unable to find app `{}` in system manifest", app_name) |
| })?; |
| (a, &a.processes[0]) |
| } |
| }; |
| |
| let context = AppRenderContext { |
| arch: &self.config.arch, |
| app, |
| process, |
| is_multi_process_app, |
| }; |
| |
| template |
| .render(&context) |
| .context("Could not render app template") |
| } |
| |
| #[must_use] |
| fn align(value: u64, alignment: u64) -> u64 { |
| debug_assert!(alignment.is_power_of_two()); |
| (value + alignment - 1) & !(alignment - 1) |
| } |
| |
| fn populate_addresses(&mut self) { |
| // Stack the apps after the kernel in flash and ram. |
| // TODO: davidroth - remove the requirement of setting the size of |
| // flash, and instead calculate it based on code size. |
| let mut next_flash_start_address = |
| self.config.base.kernel.flash_start_address + self.config.base.kernel.flash_size_bytes; |
| next_flash_start_address = Self::align(next_flash_start_address, FLASH_ALIGNMENT); |
| let mut next_ram_start_address = |
| self.config.base.kernel.ram_start_address + self.config.base.kernel.ram_size_bytes; |
| next_ram_start_address = Self::align( |
| next_ram_start_address, |
| self.config.arch.get_minimum_ram_alignment(), |
| ); |
| |
| self.config.base.arch_crate_name = self.config.arch.get_arch_crate_name(); |
| self.config.base.bare_interrupt_table_entries = |
| self.config.arch.bare_interrupt_table_entries(); |
| |
| for app in self.config.base.apps.iter_mut() { |
| app.flash_start_address = next_flash_start_address; |
| next_flash_start_address = Self::align( |
| app.flash_start_address + app.flash_size_bytes, |
| FLASH_ALIGNMENT, |
| ); |
| |
| app.ram_start_address = next_ram_start_address; |
| |
| // Calculate app RAM size as sum of process RAM sizes. |
| app.ram_size_bytes = app.processes.iter().map(|p| p.ram_size_bytes).sum(); |
| |
| let mut current_process_ram_start = app.ram_start_address; |
| for process in &mut app.processes { |
| let alignment = self.config.arch.get_ram_alignment(process.ram_size_bytes); |
| process.ram_start_address = Self::align(current_process_ram_start, alignment); |
| current_process_ram_start = process.ram_start_address + process.ram_size_bytes; |
| } |
| |
| next_ram_start_address = Self::align( |
| app.ram_start_address + app.ram_size_bytes, |
| self.config.arch.get_minimum_ram_alignment(), |
| ); |
| |
| app.start_fn_address = self |
| .config |
| .arch |
| .get_start_fn_address(app.flash_start_address); |
| |
| app.initial_sp = app.ram_start_address + app.ram_size_bytes; |
| } |
| } |
| |
| fn resolve_linked_process( |
| apps: &[system_config::AppConfig], |
| linked_process: &str, |
| ) -> Option<String> { |
| for app in apps { |
| if let Some(proc) = app.processes.iter().find(|pr| pr.name == linked_process) { |
| return Some(proc.name.clone()); |
| } |
| } |
| None |
| } |
| |
| fn populate_process_objects( |
| process: &mut system_config::ProcessConfig, |
| app_name: &str, |
| apps: &[system_config::AppConfig], |
| ) -> Result<()> { |
| let process_name = &process.name; |
| // Verify user-declared process objects |
| for object in &mut process.objects { |
| if let ObjectConfig::Process(p) = object { |
| if p.name == "process" { |
| return Err(anyhow::anyhow!( |
| "Process '{}' in App '{}' manually requested a process object named 'process'. This is reserved for the process's own handle.", |
| process_name, |
| app_name |
| )); |
| } |
| |
| if let Some(resolved_process) = |
| Self::resolve_linked_process(apps, &p.linked_process) |
| { |
| p.process_object_name = |
| std::format!("object_{}_process", resolved_process.to_lowercase()); |
| } else { |
| return Err(anyhow::anyhow!( |
| "Process '{}' in App '{}' requested handle to process object '{}' linked to '{}' but no such process found in system config", |
| process_name, |
| app_name, |
| p.name, |
| p.linked_process |
| )); |
| } |
| } |
| } |
| |
| // Add Process object for this process's own process. A fixed name is |
| // used so that generic code can be written to access a process's own |
| // process object. |
| process |
| .objects |
| .push(ObjectConfig::Process(ProcessObjectConfig { |
| name: "process".to_string(), |
| linked_process: process.name.clone(), |
| process_object_name: std::format!("object_{}_process", process.name.to_lowercase()), |
| })); |
| |
| Ok(()) |
| } |
| |
| fn populate_thread_objects(process: &mut system_config::ProcessConfig) { |
| if process.threads().count() == 1 { |
| // Mark the first thread as the main_thread. |
| if let Some(thread) = process.threads_mut().next() { |
| thread.main_thread = true; |
| } |
| } |
| |
| let main_thread = process |
| .threads() |
| .find(|t| t.main_thread) |
| .unwrap_or_else(|| { |
| panic!( |
| "Process `{}` must have at least one thread defined.", |
| process.name |
| ) |
| }); |
| |
| // Save the main_thread_name for template usage. |
| process.main_thread_name = Some(main_thread.name.clone()); |
| } |
| |
| fn populate_process_and_thread_objects(&mut self) -> Result<()> { |
| let apps = self.config.base.apps.clone(); |
| |
| for app in self.config.base.apps.iter_mut() { |
| let app_name = &app.name; |
| for process in &mut app.processes { |
| Self::populate_process_objects(process, app_name, &apps)?; |
| Self::populate_thread_objects(process); |
| } |
| } |
| Ok(()) |
| } |
| |
| fn populate_memory_mappings(&mut self) { |
| for app in self.config.base.apps.iter_mut() { |
| for process in &mut app.processes { |
| process.memory_mappings.insert( |
| 0, |
| MemoryMapping { |
| name: "flash".to_string(), |
| ty: MemoryMappingType::ReadOnlyExecutable, |
| start_address: app.flash_start_address, |
| size_bytes: app.flash_size_bytes, |
| }, |
| ); |
| process.memory_mappings.insert( |
| 1, |
| MemoryMapping { |
| name: "ram".to_string(), |
| ty: MemoryMappingType::ReadWriteData, |
| start_address: process.ram_start_address, |
| size_bytes: process.ram_size_bytes, |
| }, |
| ); |
| } |
| } |
| } |
| |
| fn populate_interrupt_table(&mut self) -> Result<()> { |
| // Add any interrupts handled by interrupt objects. |
| for app in &mut self.config.base.apps { |
| let app_name = &app.name; |
| for process in &mut app.processes { |
| for object in &mut process.objects { |
| let object_name = object.name().to_string(); |
| let &mut Interrupt(ref mut interrupt_config) = object else { |
| continue; |
| }; |
| |
| // Allow defining interrupt objects without requiring an interrupt_table |
| // to also be defined in the config. |
| if self.config.base.kernel.interrupt_table.is_none() { |
| self.config.base.kernel.interrupt_table = |
| Some(InterruptTableConfig::default()); |
| } |
| |
| let interrupt_table = self.config.base.kernel.interrupt_table.as_mut().unwrap(); |
| |
| interrupt_config.object_ref_name = |
| std::format!("INTERRUPT_OBJECT_{}_{object_name}", process.name) |
| .to_uppercase(); |
| |
| if interrupt_config.irqs.len() > 16 { |
| return Err(anyhow!( |
| "Interrupt object {} in app {} has more than 16 interrupts", |
| object_name, |
| app_name, |
| )); |
| } |
| |
| for (index, irq_config) in interrupt_config.irqs.iter().enumerate() { |
| let irq_name = &irq_config.name; |
| let irq = irq_config.number; |
| |
| if interrupt_table.table.contains_key(&irq.to_string()) { |
| return Err(anyhow!( |
| "IRQ {}={} in app {} object {} already handled.", |
| irq_name, |
| irq, |
| app_name, |
| object_name |
| )); |
| } |
| |
| let handler_name = std::format!( |
| "interrupt_handler_{}_{object_name}_{irq_name}", |
| process.name |
| ) |
| .to_lowercase(); |
| |
| interrupt_table |
| .combined_ordered_table |
| .insert(irq, handler_name.clone()); |
| |
| interrupt_config.handlers.insert(irq, handler_name.clone()); |
| |
| interrupt_config.interrupt_signal_map.insert( |
| irq_name.to_string(), |
| std::format!( |
| "Signals::INTERRUPT_{}", |
| (b'A' + u8::try_from(index).unwrap()) as char |
| ), |
| ); |
| } |
| } |
| } |
| } |
| |
| // If there's no interrupt_table defined by the user and no interrupt objects, |
| // there is no need to generate an interrupt table. |
| if self.config.base.kernel.interrupt_table.is_none() { |
| return Ok(()); |
| } |
| |
| let interrupt_table = self.config.base.kernel.interrupt_table.as_mut().unwrap(); |
| |
| // Add any kernel interrupt handlers defined in the config to the ordered list. |
| for irq_str in interrupt_table.table.keys() { |
| let irq = irq_str.parse::<u32>().unwrap(); |
| interrupt_table |
| .combined_ordered_table |
| // Use the safe wrapper handler to keep the table elements safe. |
| .insert(irq, std::format!("interrupt_handler_{irq_str}")); |
| } |
| |
| // Calculate the size of the interrupt table, which is the highest handled IRQ + 1 |
| interrupt_table.table_size = interrupt_table |
| .combined_ordered_table |
| .keys() |
| .max() |
| .map(|max_irq| max_irq + 1) |
| .unwrap_or(0) as usize; |
| |
| interrupt_table.link_section = self.config.arch.get_interrupt_table_link_section(); |
| |
| Ok(()) |
| } |
| } |
| |
| // |
| // Custom filters |
| // |
| |
| #[must_use] |
| pub fn hex(_state: &State, value: usize) -> String { |
| format!("{value:#x}") |
| } |
| |
| #[must_use] |
| pub fn to_lower_ident(_state: &State, value: String) -> String { |
| value.to_lowercase() |
| } |
| |
| #[must_use] |
| pub fn to_upper_ident(_state: &State, value: String) -> String { |
| value.to_uppercase() |
| } |