use proc_macro::TokenStream;
use quote::quote;
use proc_macro2::Literal;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{
Block, DeriveInput, Expr, Ident, LitInt, LitStr, Path, Result, Stmt, Token, bracketed,
parse_macro_input,
};
use crate::REG_PREFIX;
#[derive(Default)]
struct SampMetadata {
uid: Option<u64>,
name: Option<String>,
version: Option<(u8, u8, u8)>,
}
fn fnv1a_64(data: &[u8]) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(PRIME);
}
hash
}
fn parse_uid_str(s: &str) -> Option<u64> {
let cleaned: String = s.chars().filter(|c| *c != '_').collect();
if let Some(hex) = cleaned
.strip_prefix("0x")
.or_else(|| cleaned.strip_prefix("0X"))
{
u64::from_str_radix(hex, 16).ok()
} else {
cleaned.parse::<u64>().ok()
}
}
fn parse_version_str(s: &str) -> Option<(u8, u8, u8)> {
let mut parts = s.splitn(3, '.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts
.next()
.map(|s| s.trim_end_matches(|c: char| !c.is_ascii_digit()))
.and_then(|s| s.parse().ok())
.unwrap_or(0);
Some((major, minor, patch))
}
fn read_samp_metadata_from_content(content: &str) -> SampMetadata {
let mut meta = SampMetadata::default();
let mut in_section = false;
for line in content.lines() {
let line = line.trim();
if line == "[package.metadata.samp]" {
in_section = true;
continue;
}
if in_section {
if line.starts_with('[') {
break;
}
if let Some((key, val)) = line.split_once('=') {
let key = key.trim();
let val = val.trim().trim_matches('"').trim_matches('\'');
match key {
"uid" => meta.uid = parse_uid_str(val),
"name" => meta.name = Some(val.to_owned()),
"version" => meta.version = parse_version_str(val),
_ => {}
}
}
}
}
meta
}
fn read_samp_metadata() -> SampMetadata {
let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
return SampMetadata::default();
};
let toml_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
match std::fs::read_to_string(toml_path) {
Ok(content) => read_samp_metadata_from_content(&content),
Err(_) => SampMetadata::default(),
}
}
fn resolve_uid(meta: &SampMetadata) -> u64 {
if let Some(uid) = meta.uid {
return uid;
}
let name = std::env::var("CARGO_PKG_NAME").unwrap_or_default();
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_default();
let uid = fnv1a_64(format!("{name}@{version}").as_bytes());
persist_uid_in_cargo_toml(uid);
uid
}
fn persist_uid_in_cargo_toml(uid: u64) {
let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
return;
};
let toml_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
let Ok(content) = std::fs::read_to_string(&toml_path) else {
return;
};
if read_samp_metadata_from_content(&content).uid.is_some() {
return;
}
let new_content = format!(
"{}\n\n[package.metadata.samp]\nuid = \"{uid:#018x}\"\n",
content.trim_end()
);
let _ = std::fs::write(&toml_path, new_content);
}
enum Constructor {
Block(Vec<Stmt>),
Default(Path),
}
struct InitPlugin {
natives_list: Option<Punctuated<Path, Token![,]>>,
constructor: Constructor,
explicit_uid: Option<Expr>,
explicit_name: Option<LitStr>,
explicit_version: Option<(u8, u8, u8)>,
}
impl Parse for InitPlugin {
fn parse(input: ParseStream) -> Result<Self> {
let mut natives_list = None;
let mut default_type: Option<Path> = None;
let mut explicit_uid: Option<Expr> = None;
let mut explicit_name: Option<LitStr> = None;
let mut explicit_version: Option<(u8, u8, u8)> = None;
loop {
if input.peek(Token![type]) {
let _: Token![type] = input.parse()?;
let _: Token![:] = input.parse()?;
default_type = Some(input.parse()?);
let _: Option<Token![,]> = input.parse()?;
} else if input.peek(Ident) {
let fork = input.fork();
let ident: Ident = fork.parse()?;
match ident.to_string().as_str() {
"natives" => {
let _: Ident = input.parse()?;
let _: Token![:] = input.parse()?;
let content;
let _ = bracketed!(content in input);
natives_list = Some(Punctuated::parse_terminated(&content)?);
let _: Option<Token![,]> = input.parse()?;
}
"uid" => {
let _: Ident = input.parse()?;
let _: Token![:] = input.parse()?;
explicit_uid = Some(input.parse()?);
let _: Option<Token![,]> = input.parse()?;
}
"component_name" => {
let _: Ident = input.parse()?;
let _: Token![:] = input.parse()?;
explicit_name = Some(input.parse()?);
let _: Option<Token![,]> = input.parse()?;
}
"component_version" => {
let _: Ident = input.parse()?;
let _: Token![:] = input.parse()?;
let content;
syn::parenthesized!(content in input);
let major: LitInt = content.parse()?;
let _: Token![,] = content.parse()?;
let minor: LitInt = content.parse()?;
let _: Token![,] = content.parse()?;
let patch: LitInt = content.parse()?;
explicit_version = Some((
major.base10_parse()?,
minor.base10_parse()?,
patch.base10_parse()?,
));
let _: Option<Token![,]> = input.parse()?;
}
_ => break,
}
} else {
break;
}
}
let constructor = if let Some(ty) = default_type {
Constructor::Default(ty)
} else {
Constructor::Block(input.call(Block::parse_within)?)
};
Ok(InitPlugin {
natives_list,
constructor,
explicit_uid,
explicit_name,
explicit_version,
})
}
}
pub fn create_plugin(input: TokenStream) -> TokenStream {
let plugin = parse_macro_input!(input as InitPlugin);
let natives = gen_natives_list(&plugin);
let supports_body = gen_samp_constructor(&plugin.constructor);
let samp_entry_points = gen_samp_entry_points(&natives, &supports_body);
let samp_only = std::env::var("CARGO_FEATURE_SAMP_ONLY").is_ok();
let cargo_meta = read_samp_metadata();
let omp_entry_point = if samp_only {
quote! {}
} else {
gen_omp_entry_point(&plugin, &cargo_meta, &natives)
};
let generated = quote! {
#samp_entry_points
#omp_entry_point
};
generated.into()
}
fn gen_natives_list(plugin: &InitPlugin) -> proc_macro2::TokenStream {
plugin
.natives_list
.iter()
.flatten()
.map(|path| {
let mut path = path.clone();
if let Some(last_part) = path.segments.last_mut() {
let span = last_part.ident.span();
last_part.ident = Ident::new(&format!("{}{}", REG_PREFIX, last_part.ident), span);
}
quote!(#path(),)
})
.collect()
}
fn gen_samp_constructor(constructor: &Constructor) -> proc_macro2::TokenStream {
match constructor {
Constructor::Block(stmts) => quote! {
let constructor = || { #(#stmts)* };
samp::plugin::initialize(constructor);
},
Constructor::Default(ty) => quote! {
samp::plugin::initialize(<#ty as Default>::default);
},
}
}
fn gen_samp_entry_points(
natives: &proc_macro2::TokenStream,
supports_body: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
quote! {
#[unsafe(no_mangle)]
pub extern "system" fn Load(server_data: *const usize) -> i32 {
samp::interlayer::load(server_data);
return 1;
}
#[unsafe(no_mangle)]
pub extern "system" fn Unload() {
samp::interlayer::unload();
}
#[unsafe(no_mangle)]
pub extern "system" fn AmxLoad(amx: *mut samp::raw::types::AMX) {
let natives = vec![#natives];
samp::interlayer::amx_load(amx, &natives);
}
#[unsafe(no_mangle)]
pub extern "system" fn AmxUnload(amx: *mut samp::raw::types::AMX) {
samp::interlayer::amx_unload(amx);
}
#[unsafe(no_mangle)]
pub extern "system" fn Supports() -> u32 {
#supports_body
samp::interlayer::supports()
}
#[unsafe(no_mangle)]
pub extern "system" fn ProcessTick() {
samp::interlayer::tick(samp::plugin::TickSource::SaMp);
}
}
}
fn resolve_uid_expr(plugin: &InitPlugin, cargo_meta: &SampMetadata) -> proc_macro2::TokenStream {
if let Some(ref expr) = plugin.explicit_uid {
quote! { #expr }
} else {
let lit = Literal::u64_suffixed(resolve_uid(cargo_meta));
quote! { #lit }
}
}
fn resolve_component_name(plugin: &InitPlugin, cargo_meta: &SampMetadata) -> String {
if let Some(ref name) = plugin.explicit_name {
name.value()
} else {
cargo_meta
.name
.clone()
.or_else(|| std::env::var("CARGO_PKG_NAME").ok())
.unwrap_or_default()
}
}
fn resolve_component_version(plugin: &InitPlugin, cargo_meta: &SampMetadata) -> (u8, u8, u8) {
plugin
.explicit_version
.or(cargo_meta.version)
.or_else(|| {
std::env::var("CARGO_PKG_VERSION")
.ok()
.and_then(|v| parse_version_str(&v))
})
.unwrap_or((1, 0, 0))
}
fn gen_omp_constructor(constructor: &Constructor) -> proc_macro2::TokenStream {
match constructor {
Constructor::Block(stmts) => quote! {
let constructor = || { #(#stmts)* };
samp::interlayer::omp_initialize(constructor);
},
Constructor::Default(ty) => quote! {
samp::interlayer::omp_initialize(<#ty as Default>::default);
},
}
}
#[allow(clippy::too_many_lines)]
fn gen_omp_entry_point(
plugin: &InitPlugin,
cargo_meta: &SampMetadata,
natives: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let uid_expr = resolve_uid_expr(plugin, cargo_meta);
let name_str = resolve_component_name(plugin, cargo_meta);
let name_len: usize = name_str.len();
let name_bytes: Vec<u8> = name_str.bytes().collect();
let name_sv = {
let name_lit = name_str.clone();
quote! {{
const __S: &str = #name_lit;
samp::omp::types::StringView { data: __S.as_ptr(), len: #name_len }
}}
};
let (major, minor, patch) = resolve_component_version(plugin, cargo_meta);
let omp_initialize = gen_omp_constructor(&plugin.constructor);
quote! {
mod __omp_component {
use super::*;
use samp::omp::component::*;
use samp::omp::types::*;
macro_rules! __def_comp_fns {
($abi:literal) => {
#[cfg(not(target_env = "msvc"))]
pub unsafe extern "C" fn comp_name(_this: *const OmpComponent) -> StringView {
#name_sv
}
#[cfg(target_env = "msvc")]
#[unsafe(no_mangle)]
static COMP_NAME_BYTES: [u8; #name_len] = [#(#name_bytes),*];
#[cfg(target_env = "msvc")]
#[unsafe(naked)]
pub unsafe extern "thiscall" fn comp_name() {
core::arch::naked_asm!(
"mov eax, [esp+4]",
"mov ecx, offset {data_ptr}",
"mov dword ptr [eax], ecx",
"mov dword ptr [eax+4], {data_len}",
"ret 4",
data_ptr = sym COMP_NAME_BYTES,
data_len = const #name_len,
);
}
#[cfg(not(target_env = "msvc"))]
pub unsafe extern "C" fn comp_version(_this: *const OmpComponent) -> SemanticVersion {
SemanticVersion::new(#major, #minor, #patch)
}
#[cfg(target_env = "msvc")]
#[unsafe(naked)]
pub unsafe extern "thiscall" fn comp_version() {
core::arch::naked_asm!(
"mov eax, [esp+4]",
"mov dword ptr [eax], {sver_lo}",
"mov word ptr [eax+4], 0",
"ret 4",
sver_lo = const ((#major as u32) | ((#minor as u32) << 8) | ((#patch as u32) << 16)),
);
}
pub unsafe extern $abi fn comp_on_load(_this: *mut OmpComponent, core: *mut ICore) {
let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
samp::interlayer::omp_load(core);
}));
}
pub unsafe extern $abi fn comp_on_init(
_this: *mut OmpComponent,
components: *mut IComponentList,
) {
let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
let component_list_ptr = components as *mut samp::omp::server::ServerComponentList;
unsafe { samp::interlayer::omp_on_init(component_list_ptr) };
}));
}
#[cfg(not(target_env = "msvc"))]
pub unsafe extern "C" fn comp_on_ready(_this: *mut OmpComponent) {
let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
samp::interlayer::omp_on_ready();
}));
}
pub unsafe extern $abi fn comp_on_free(
_this: *mut OmpComponent,
_component: *mut OmpComponent,
) {
let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
samp::interlayer::omp_on_free();
}));
}
#[cfg(not(target_env = "msvc"))]
pub unsafe extern "C" fn comp_free(_this: *mut OmpComponent) {
let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
samp::interlayer::omp_cleanup();
samp::interlayer::unload();
let _ = unsafe { Box::from_raw(_this) };
}));
}
#[cfg(not(target_env = "msvc"))]
pub unsafe extern "C" fn comp_reset(_this: *mut OmpComponent) {}
};
}
#[cfg(not(target_env = "msvc"))]
__def_comp_fns!("C");
#[cfg(target_env = "msvc")]
__def_comp_fns!("thiscall");
#[cfg(target_env = "msvc")]
pub unsafe extern "thiscall" fn comp_on_ready() {
let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
samp::interlayer::omp_on_ready();
}));
}
#[cfg(target_env = "msvc")]
pub unsafe extern "thiscall" fn comp_free() {
let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
samp::interlayer::omp_cleanup();
samp::interlayer::unload();
}));
}
#[cfg(target_env = "msvc")]
pub unsafe extern "thiscall" fn comp_reset() {}
#[cfg(not(target_env = "msvc"))]
static VTABLE: IComponentVTable = IComponentVTable {
get_extension: samp::omp::component::ext_get_extension,
add_extension: samp::omp::component::ext_add_extension,
remove_extension_ptr: samp::omp::component::ext_remove_extension_ptr,
remove_extension_uid: samp::omp::component::ext_remove_extension_uid,
destructor: samp::omp::component::ext_destructor,
destructor_deleting: samp::omp::component::ext_destructor_deleting,
supported_version: samp::omp::component::comp_supported_version,
component_name: comp_name,
component_type: samp::omp::component::comp_component_type,
component_version: comp_version,
on_load: comp_on_load,
on_init: comp_on_init,
on_ready: comp_on_ready,
on_free: comp_on_free,
provide_configuration: samp::omp::component::comp_provide_configuration,
free: comp_free,
reset: comp_reset,
};
#[cfg(target_env = "msvc")]
static VTABLE: IComponentVTable = IComponentVTable {
get_extension: samp::omp::component::ext_get_extension,
add_extension: samp::omp::component::ext_add_extension,
remove_extension_ptr: samp::omp::component::ext_remove_extension_ptr,
remove_extension_uid: samp::omp::component::ext_remove_extension_uid,
destructor: samp::omp::component::ext_destructor,
supported_version: samp::omp::component::comp_supported_version,
component_name: comp_name,
component_type: samp::omp::component::comp_component_type,
component_version: comp_version,
on_load: comp_on_load,
on_init: comp_on_init,
on_ready: comp_on_ready,
on_free: comp_on_free,
provide_configuration: samp::omp::component::comp_provide_configuration,
free: comp_free,
reset: comp_reset,
};
#[cfg(not(target_env = "msvc"))]
static UID_VTABLE: IUIDProviderVTable = IUIDProviderVTable {
destructor_complete: samp::omp::component::uid_destructor_noop,
destructor_deleting: samp::omp::component::uid_destructor_noop,
get_uid: samp::omp::component::uid_get_uid,
};
#[cfg(target_env = "msvc")]
static UID_VTABLE: IUIDProviderVTable = IUIDProviderVTable {
get_uid: samp::omp::component::uid_get_uid,
};
#[unsafe(no_mangle)]
pub extern "C" fn ComponentEntryPoint() -> *mut OmpComponent {
#omp_initialize
samp::interlayer::omp_store_natives(vec![#natives]);
let component = Box::new(OmpComponent::new(&VTABLE, &UID_VTABLE, #uid_expr));
Box::into_raw(component)
}
}
}
}
pub fn derive_samp_plugin(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let generated = quote! {
impl #impl_generics samp::prelude::SampPlugin for #name #ty_generics #where_clause {}
};
generated.into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fnv1a_64_empty_returns_offset_basis() {
assert_eq!(fnv1a_64(b""), 0xcbf2_9ce4_8422_2325);
}
#[test]
fn fnv1a_64_known_vector_a() {
assert_eq!(fnv1a_64(b"a"), 0xaf63_dc4c_8601_ec8c);
}
#[test]
fn fnv1a_64_is_deterministic() {
assert_eq!(fnv1a_64(b"plugin@1.0.0"), fnv1a_64(b"plugin@1.0.0"));
}
#[test]
fn fnv1a_64_different_inputs_differ() {
assert_ne!(fnv1a_64(b"plugin@1.0.0"), fnv1a_64(b"plugin@1.0.1"));
}
#[test]
fn parse_uid_hex_lowercase() {
assert_eq!(parse_uid_str("0x12_3abc"), Some(0x12_3abc));
}
#[test]
fn parse_uid_hex_uppercase_prefix() {
assert_eq!(parse_uid_str("0X123ABC"), Some(0x12_3abc));
}
#[test]
fn parse_uid_decimal() {
assert_eq!(parse_uid_str("12345"), Some(12345));
}
#[test]
fn parse_uid_max_u64() {
assert_eq!(parse_uid_str("0xFFFFFFFFFFFFFFFF"), Some(u64::MAX));
}
#[test]
fn parse_uid_invalid_returns_none() {
assert_eq!(parse_uid_str("invalid"), None);
}
#[test]
fn parse_uid_empty_returns_none() {
assert_eq!(parse_uid_str(""), None);
}
#[test]
fn parse_version_full() {
assert_eq!(parse_version_str("1.2.3"), Some((1, 2, 3)));
}
#[test]
fn parse_version_zeros() {
assert_eq!(parse_version_str("0.0.0"), Some((0, 0, 0)));
}
#[test]
fn parse_version_max_values() {
assert_eq!(parse_version_str("255.255.255"), Some((255, 255, 255)));
}
#[test]
fn parse_version_with_prerelease_suffix() {
assert_eq!(parse_version_str("1.2.3-beta"), Some((1, 2, 3)));
}
#[test]
fn parse_version_missing_patch_defaults_zero() {
assert_eq!(parse_version_str("1.2"), Some((1, 2, 0)));
}
#[test]
fn parse_version_missing_minor_returns_none() {
assert_eq!(parse_version_str("1"), None);
}
#[test]
fn parse_version_invalid_returns_none() {
assert_eq!(parse_version_str("invalid"), None);
}
#[test]
fn parse_version_invalid_component_returns_none() {
assert_eq!(parse_version_str("1.invalid.3"), None);
}
#[test]
fn metadata_empty_content_returns_default() {
let meta = read_samp_metadata_from_content("");
assert!(meta.uid.is_none());
assert!(meta.name.is_none());
assert!(meta.version.is_none());
}
#[test]
fn metadata_reads_uid() {
let content = "[package.metadata.samp]\nuid = \"0x4D455550CAFEBABE\"\n";
let meta = read_samp_metadata_from_content(content);
assert_eq!(meta.uid, Some(0x4D45_5550_CAFE_BABE));
}
#[test]
fn metadata_reads_name() {
let content = "[package.metadata.samp]\nname = \"MeuPlugin\"\n";
let meta = read_samp_metadata_from_content(content);
assert_eq!(meta.name.as_deref(), Some("MeuPlugin"));
}
#[test]
fn metadata_reads_version() {
let content = "[package.metadata.samp]\nversion = \"1.2.3\"\n";
let meta = read_samp_metadata_from_content(content);
assert_eq!(meta.version, Some((1, 2, 3)));
}
#[test]
fn metadata_reads_all_fields() {
let content = "[package.metadata.samp]\nuid = \"0xDEADBEEF\"\nname = \"Plugin\"\nversion = \"2.0.1\"\n";
let meta = read_samp_metadata_from_content(content);
assert_eq!(meta.uid, Some(0xDEAD_BEEF));
assert_eq!(meta.name.as_deref(), Some("Plugin"));
assert_eq!(meta.version, Some((2, 0, 1)));
}
#[test]
fn metadata_no_section_returns_default() {
let content = "[package]\nname = \"meu-crate\"\nversion = \"1.0.0\"\n";
let meta = read_samp_metadata_from_content(content);
assert!(meta.uid.is_none());
}
#[test]
fn metadata_stops_at_next_section() {
let content =
"[package.metadata.samp]\nuid = \"0x1234\"\n[other.section]\nuid = \"0x5678\"\n";
let meta = read_samp_metadata_from_content(content);
assert_eq!(meta.uid, Some(0x1234));
}
#[test]
fn metadata_accepts_single_quoted_values() {
let content = "[package.metadata.samp]\nname = 'MinhaPlugin'\n";
let meta = read_samp_metadata_from_content(content);
assert_eq!(meta.name.as_deref(), Some("MinhaPlugin"));
}
}