use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::sync::{Arc, Mutex};
use fusevm::Value;
use super::abi::{RawStub, TclObj, TclStubs, TCL_ERROR, TCL_OK};
use super::generated::TCL_NAMES;
use super::host::{self, HostInterp};
use super::interp;
use super::obj;
use super::trace::{note, record, Table};
use crate::runtime::{
self, arm_var_traces, set_var_trace_sink, to_tcl_string, TraceOp, Traced, VarTraceSink,
};
macro_rules! entered {
($name:literal) => {
record(
Table::Tcl,
TCL_NAMES
.iter()
.position(|n| *n == $name)
.expect("no such slot"),
)
};
}
pub const TCL_GLOBAL_ONLY: c_int = 1;
pub const TCL_LEAVE_ERR_MSG: c_int = 0x200;
pub const TCL_TRACE_READS: c_int = 0x10;
pub const TCL_TRACE_WRITES: c_int = 0x20;
pub const TCL_TRACE_UNSETS: c_int = 0x40;
pub const TCL_TRACE_DESTROYED: c_int = 0x80;
pub const TCL_TRACE_ARRAY: c_int = 0x800;
pub const TCL_TRACE_RESULT_DYNAMIC: c_int = 0x8000;
pub const TCL_TRACE_RESULT_OBJECT: c_int = 0x10000;
const TRACE_FLAG_MASK: c_int = TCL_TRACE_READS
| TCL_TRACE_WRITES
| TCL_TRACE_UNSETS
| TCL_TRACE_ARRAY
| TCL_TRACE_RESULT_DYNAMIC
| TCL_TRACE_RESULT_OBJECT;
const TCL_LINK_INT: c_int = 1;
const TCL_LINK_DOUBLE: c_int = 2;
const TCL_LINK_BOOLEAN: c_int = 3;
const TCL_LINK_STRING: c_int = 4;
const TCL_LINK_WIDE_INT: c_int = 5;
const TCL_LINK_CHAR: c_int = 6;
const TCL_LINK_UCHAR: c_int = 7;
const TCL_LINK_SHORT: c_int = 8;
const TCL_LINK_USHORT: c_int = 9;
const TCL_LINK_UINT: c_int = 10;
const TCL_LINK_FLOAT: c_int = 13;
const TCL_LINK_WIDE_UINT: c_int = 14;
const TCL_LINK_READ_ONLY: c_int = 0x80;
const LINK_READ_ONLY: c_int = 1;
const LINK_BEING_UPDATED: c_int = 2;
type VarTraceProc = unsafe extern "C" fn(
*mut c_void,
*mut c_void,
*const c_char,
*const c_char,
c_int,
) -> *mut c_char;
#[derive(Clone)]
struct VarTrace {
part1: String,
part2: Option<String>,
flags: c_int,
proc_: usize,
client_data: usize,
}
struct Link {
var_name: String,
addr: usize,
ty: c_int,
flags: c_int,
last: LastValue,
}
#[derive(Clone, Copy, PartialEq)]
enum LastValue {
Int(i64),
Float(f64),
Text(usize),
None,
}
#[derive(Default)]
struct Registry {
traces: Vec<VarTrace>,
links: Vec<Link>,
active: Vec<String>,
}
static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
fn with<R>(f: impl FnOnce(&mut Registry) -> R) -> R {
let mut guard = REGISTRY.lock().expect("variable trace registry");
let registry = guard.get_or_insert_with(Registry::default);
let out = f(registry);
arm_var_traces(!registry.traces.is_empty());
out
}
pub fn counts() -> (usize, usize) {
with(|r| (r.traces.len(), r.links.len()))
}
pub fn install_sink() {
set_var_trace_sink(Arc::new(Sink));
}
struct Sink;
impl VarTraceSink for Sink {
fn traced(&self, name: &str) -> Traced {
with(|r| {
let mut watched = Traced::default();
for t in r.traces.iter().filter(|t| t.part1 == name) {
watched.reads |= t.flags & TCL_TRACE_READS != 0;
watched.writes |= t.flags & TCL_TRACE_WRITES != 0;
watched.unsets |= t.flags & TCL_TRACE_UNSETS != 0;
}
watched
})
}
fn fire(&self, name: &str, op: TraceOp) -> Result<(), String> {
fire_traces(name, None, op)
}
}
fn fire_traces(part1: &str, part2: Option<&str>, op: TraceOp) -> Result<(), String> {
let bit = match op {
TraceOp::Read => TCL_TRACE_READS,
TraceOp::Write => TCL_TRACE_WRITES,
TraceOp::Unset => TCL_TRACE_UNSETS,
};
let due: Vec<VarTrace> = with(|r| {
if r.active.iter().any(|n| n == part1) {
return Vec::new();
}
let due: Vec<VarTrace> = r
.traces
.iter()
.rev()
.filter(|t| t.part1 == part1 && t.part2.as_deref() == part2 && t.flags & bit != 0)
.cloned()
.collect();
if !due.is_empty() {
r.active.push(part1.to_string());
}
due
});
if due.is_empty() {
return Ok(());
}
let flags = bit
| TCL_GLOBAL_ONLY
| if matches!(op, TraceOp::Unset) {
TCL_TRACE_DESTROYED
} else {
0
};
let outcome = run_procs(&due, part1, part2, flags);
with(|r| r.active.retain(|n| n != part1));
outcome
}
fn run_procs(
due: &[VarTrace],
part1: &str,
part2: Option<&str>,
flags: c_int,
) -> Result<(), String> {
let interp = interp::current() as *mut c_void;
if interp.is_null() {
return Ok(());
}
let name1 = CString::new(part1).map_err(|_| "a variable name with a NUL in it".to_string())?;
let name2 = part2.and_then(|p| CString::new(p).ok());
let name2_ptr = name2
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr() as *const c_char);
for t in due {
let message = unsafe {
let f: VarTraceProc = std::mem::transmute(t.proc_);
f(
t.client_data as *mut c_void,
interp,
name1.as_ptr() as *const c_char,
name2_ptr,
flags,
)
};
if message.is_null() {
continue;
}
if flags & TCL_TRACE_UNSETS != 0 {
continue;
}
return Err(trace_result(t.flags, message));
}
Ok(())
}
fn trace_result(flags: c_int, message: *mut c_char) -> String {
unsafe {
if flags & TCL_TRACE_RESULT_OBJECT != 0 {
return obj::text_of(message as *mut TclObj);
}
let text = String::from_utf8_lossy(CStr::from_ptr(message).to_bytes()).into_owned();
if flags & TCL_TRACE_RESULT_DYNAMIC != 0 {
libc::free(message as *mut c_void);
}
text
}
}
unsafe extern "C" fn trace_var2(
_interp: *mut c_void,
part1: *const c_char,
part2: *const c_char,
flags: c_int,
proc_: *mut c_void,
client_data: *mut c_void,
) -> c_int {
entered!("tcl_TraceVar2");
let (name, index) = name_parts(part1, part2);
note("TraceVar2", &name);
if proc_.is_null() {
return TCL_ERROR;
}
with(|r| {
r.traces.push(VarTrace {
part1: name,
part2: index,
flags: flags & TRACE_FLAG_MASK,
proc_: proc_ as usize,
client_data: client_data as usize,
});
});
TCL_OK
}
unsafe extern "C" fn untrace_var2(
_interp: *mut c_void,
part1: *const c_char,
part2: *const c_char,
flags: c_int,
proc_: *mut c_void,
client_data: *mut c_void,
) {
entered!("tcl_UntraceVar2");
let (name, index) = name_parts(part1, part2);
let flags = flags & TRACE_FLAG_MASK;
with(|r| {
if let Some(at) = r.traces.iter().rposition(|t| {
t.part1 == name
&& t.part2 == index
&& t.flags == flags
&& t.proc_ == proc_ as usize
&& t.client_data == client_data as usize
}) {
r.traces.remove(at);
}
});
}
unsafe extern "C" fn var_trace_info2(
_interp: *mut c_void,
part1: *const c_char,
part2: *const c_char,
_flags: c_int,
proc_: *mut c_void,
prev_client_data: *mut c_void,
) -> *mut c_void {
entered!("tcl_VarTraceInfo2");
let (name, index) = name_parts(part1, part2);
with(|r| {
let mut it = r
.traces
.iter()
.rev()
.filter(|t| t.part1 == name && t.part2 == index)
.peekable();
if !prev_client_data.is_null() {
let mut seen = false;
for t in it.by_ref() {
if t.client_data == prev_client_data as usize && t.proc_ == proc_ as usize {
seen = true;
break;
}
}
if !seen {
return std::ptr::null_mut();
}
}
for t in it {
if t.proc_ == proc_ as usize {
return t.client_data as *mut c_void;
}
}
std::ptr::null_mut()
})
}
pub unsafe fn link_var(
interp: *mut c_void,
name: *const c_char,
addr: *mut c_void,
ty: c_int,
) -> c_int {
let var_name = c_string(name);
note("LinkVar", &var_name);
if with(|r| r.links.iter().any(|l| l.var_name == var_name)) {
host::set_result_bytes(
interp,
format!("variable '{var_name}' is already linked").as_bytes(),
);
return TCL_ERROR;
}
let mut link = Link {
var_name: var_name.clone(),
addr: addr as usize,
ty: ty & !TCL_LINK_READ_ONLY,
flags: if ty & TCL_LINK_READ_ONLY != 0 {
LINK_READ_ONLY
} else {
0
},
last: LastValue::None,
};
let Some(value) = obj_value(&mut link) else {
host::set_result_bytes(
interp,
format!("bad linked variable type {}", link.ty).as_bytes(),
);
return TCL_ERROR;
};
let Some(shared) = shared_of(interp) else {
with(|r| r.links.push(link));
return TCL_OK;
};
if runtime::set_global_of(&shared, &var_name, value).is_err() {
return TCL_ERROR;
}
with(|r| {
r.links.push(link);
r.traces.push(VarTrace {
part1: var_name,
part2: None,
flags: TCL_TRACE_READS | TCL_TRACE_WRITES | TCL_TRACE_UNSETS,
proc_: link_trace_proc as *const () as usize,
client_data: 0,
});
});
TCL_OK
}
unsafe extern "C" fn unlink_var(_interp: *mut c_void, name: *const c_char) {
entered!("tcl_UnlinkVar");
let var_name = c_string(name);
with(|r| {
r.links.retain(|l| l.var_name != var_name);
r.traces
.retain(|t| !(t.part1 == var_name && t.proc_ == link_trace_proc as *const () as usize));
});
}
unsafe extern "C" fn update_linked_var(interp: *mut c_void, name: *const c_char) {
entered!("tcl_UpdateLinkedVar");
let var_name = c_string(name);
let Some(shared) = shared_of(interp) else {
return;
};
let value = with(|r| {
let link = r.links.iter_mut().find(|l| l.var_name == var_name)?;
link.flags |= LINK_BEING_UPDATED;
obj_value(link)
});
if let Some(value) = value {
let _ = runtime::set_global_of(&shared, &var_name, value);
}
with(|r| {
if let Some(link) = r.links.iter_mut().find(|l| l.var_name == var_name) {
link.flags &= !LINK_BEING_UPDATED;
}
});
}
unsafe extern "C" fn link_trace_proc(
_client_data: *mut c_void,
interp: *mut c_void,
name1: *const c_char,
_name2: *const c_char,
flags: c_int,
) -> *mut c_char {
let var_name = c_string(name1);
let Some(shared) = shared_of(interp) else {
return std::ptr::null_mut();
};
if flags & TCL_TRACE_UNSETS != 0 {
let value = with(|r| {
let link = r.links.iter_mut().find(|l| l.var_name == var_name)?;
obj_value(link)
});
if let Some(value) = value {
let _ = runtime::set_global_of(&shared, &var_name, value);
}
return std::ptr::null_mut();
}
if with(|r| {
r.links
.iter()
.any(|l| l.var_name == var_name && l.flags & LINK_BEING_UPDATED != 0)
}) {
return std::ptr::null_mut();
}
if flags & TCL_TRACE_READS != 0 {
let value = with(|r| {
let link = r.links.iter_mut().find(|l| l.var_name == var_name)?;
let before = link.last;
let value = obj_value(link)?;
(before != link.last || matches!(link.ty, TCL_LINK_STRING)).then_some(value)
});
if let Some(value) = value {
let _ = runtime::set_global_of(&shared, &var_name, value);
}
return std::ptr::null_mut();
}
let read_only = with(|r| {
r.links
.iter()
.any(|l| l.var_name == var_name && l.flags & LINK_READ_ONLY != 0)
});
if read_only {
let value = with(|r| {
let link = r.links.iter_mut().find(|l| l.var_name == var_name)?;
obj_value(link)
});
if let Some(value) = value {
let _ = runtime::set_global_of(&shared, &var_name, value);
}
return c"linked variable is read-only".as_ptr() as *mut c_char;
}
let Some(written) = runtime::global_of(&shared, &var_name) else {
return c"internal error: linked variable couldn't be read".as_ptr() as *mut c_char;
};
let restore = with(|r| {
let link = r.links.iter_mut().find(|l| l.var_name == var_name)?;
match store_value(link, &written) {
Ok(()) => None,
Err(message) => Some((obj_value(link), message)),
}
});
match restore {
None => std::ptr::null_mut(),
Some((value, message)) => {
if let Some(value) = value {
let _ = runtime::set_global_of(&shared, &var_name, value);
}
message.as_ptr() as *mut c_char
}
}
}
fn obj_value(link: &mut Link) -> Option<Value> {
let addr = link.addr as *const c_void;
if addr.is_null() {
return None;
}
unsafe {
let value = match link.ty {
TCL_LINK_INT => integral(link, *(addr as *const c_int) as i64),
TCL_LINK_BOOLEAN => {
let raw = *(addr as *const c_int);
link.last = LastValue::Int(raw as i64);
Value::Bool(raw != 0)
}
TCL_LINK_WIDE_INT => integral(link, *(addr as *const i64)),
TCL_LINK_CHAR => integral(link, *(addr as *const i8) as i64),
TCL_LINK_UCHAR => integral(link, *(addr as *const u8) as i64),
TCL_LINK_SHORT => integral(link, *(addr as *const i16) as i64),
TCL_LINK_USHORT => integral(link, *(addr as *const u16) as i64),
TCL_LINK_UINT => integral(link, *(addr as *const u32) as i64),
TCL_LINK_WIDE_UINT => {
let raw = *(addr as *const u64);
link.last = LastValue::Int(raw as i64);
Value::Str(Arc::new(raw.to_string()))
}
TCL_LINK_DOUBLE => real(link, *(addr as *const f64)),
TCL_LINK_FLOAT => real(link, *(addr as *const f32) as f64),
TCL_LINK_STRING => {
let p = *(addr as *const *const c_char);
link.last = LastValue::Text(p as usize);
if p.is_null() {
Value::Str(Arc::new("NULL".to_string()))
} else {
Value::Str(Arc::new(
String::from_utf8_lossy(CStr::from_ptr(p).to_bytes()).into_owned(),
))
}
}
_ => return None,
};
Some(value)
}
}
fn integral(link: &mut Link, raw: i64) -> Value {
link.last = LastValue::Int(raw);
Value::Int(raw)
}
fn real(link: &mut Link, raw: f64) -> Value {
link.last = LastValue::Float(raw);
Value::Float(raw)
}
fn store_value(link: &mut Link, value: &Value) -> Result<(), &'static str> {
let addr = link.addr as *mut c_void;
if addr.is_null() {
return Err("internal error: bad linked variable type");
}
let text = to_tcl_string(value);
unsafe {
match link.ty {
TCL_LINK_INT => {
let n = wide(&text).ok_or("variable must have integer value")?;
let n: i32 = n
.try_into()
.map_err(|_| "variable must have integer value")?;
*(addr as *mut c_int) = n;
link.last = LastValue::Int(n as i64);
}
TCL_LINK_BOOLEAN => {
let b = crate::runtime::tcl_bool(value)
.map_err(|_| "variable must have boolean value")?;
*(addr as *mut c_int) = c_int::from(b);
link.last = LastValue::Int(i64::from(b));
}
TCL_LINK_WIDE_INT => {
let n = wide(&text).ok_or("variable must have wide integer value")?;
*(addr as *mut i64) = n;
link.last = LastValue::Int(n);
}
TCL_LINK_CHAR => {
let n = narrow::<i8>(&text, "variable must have char value")?;
*(addr as *mut i8) = n;
link.last = LastValue::Int(n as i64);
}
TCL_LINK_UCHAR => {
let n = narrow::<u8>(&text, "variable must have unsigned char value")?;
*(addr as *mut u8) = n;
link.last = LastValue::Int(n as i64);
}
TCL_LINK_SHORT => {
let n = narrow::<i16>(&text, "variable must have short value")?;
*(addr as *mut i16) = n;
link.last = LastValue::Int(n as i64);
}
TCL_LINK_USHORT => {
let n = narrow::<u16>(&text, "variable must have unsigned short value")?;
*(addr as *mut u16) = n;
link.last = LastValue::Int(n as i64);
}
TCL_LINK_UINT => {
let n = narrow::<u32>(&text, "variable must have unsigned int value")?;
*(addr as *mut u32) = n;
link.last = LastValue::Int(n as i64);
}
TCL_LINK_WIDE_UINT => {
let n: u64 = text
.trim()
.parse()
.map_err(|_| "variable must have unsigned wide int value")?;
*(addr as *mut u64) = n;
link.last = LastValue::Int(n as i64);
}
TCL_LINK_DOUBLE => {
let f: f64 = text
.trim()
.parse()
.map_err(|_| "variable must have real value")?;
*(addr as *mut f64) = f;
link.last = LastValue::Float(f);
}
TCL_LINK_FLOAT => {
let f: f64 = text
.trim()
.parse()
.map_err(|_| "variable must have float value")?;
*(addr as *mut f32) = f as f32;
link.last = LastValue::Float(f);
}
TCL_LINK_STRING => {
let bytes = text.as_bytes();
let fresh = libc::malloc(bytes.len() + 1) as *mut c_char;
if fresh.is_null() {
return Err("internal error: linked variable couldn't be read");
}
std::ptr::copy_nonoverlapping(bytes.as_ptr(), fresh as *mut u8, bytes.len());
*fresh.add(bytes.len()) = 0;
let slot = addr as *mut *mut c_char;
if !(*slot).is_null() {
libc::free(*slot as *mut c_void);
}
*slot = fresh;
link.last = LastValue::Text(fresh as usize);
}
_ => return Err("internal error: bad linked variable type"),
}
}
Ok(())
}
fn wide(text: &str) -> Option<i64> {
text.trim().parse().ok()
}
fn narrow<T: TryFrom<i64>>(text: &str, refusal: &'static str) -> Result<T, &'static str> {
wide(text).ok_or(refusal)?.try_into().map_err(|_| refusal)
}
unsafe extern "C" fn obj_get_var2(
interp: *mut c_void,
part1: *mut TclObj,
part2: *mut TclObj,
_flags: c_int,
) -> *mut TclObj {
entered!("tcl_ObjGetVar2");
let name = obj::text_of(part1);
let Some(shared) = shared_of(interp) else {
return std::ptr::null_mut();
};
if !part2.is_null() {
return std::ptr::null_mut();
}
match runtime::global_of(&shared, &name) {
Some(value) => cached_obj(interp, &name, &value),
None => std::ptr::null_mut(),
}
}
unsafe extern "C" fn obj_set_var2(
interp: *mut c_void,
part1: *mut TclObj,
part2: *mut TclObj,
value: *mut TclObj,
flags: c_int,
) -> *mut TclObj {
entered!("tcl_ObjSetVar2");
let name = obj::text_of(part1);
note("ObjSetVar2", &name);
if !part2.is_null() {
return std::ptr::null_mut();
}
set_scalar(interp, &name, obj::to_value(value), flags)
}
unsafe extern "C" fn set_var2_ex(
interp: *mut c_void,
part1: *const c_char,
part2: *const c_char,
value: *mut TclObj,
flags: c_int,
) -> *mut TclObj {
entered!("tcl_SetVar2Ex");
let (name, index) = name_parts(part1, part2);
note("SetVar2Ex", &name);
if index.is_some() {
return std::ptr::null_mut();
}
set_scalar(interp, &name, obj::to_value(value), flags)
}
unsafe extern "C" fn unset_var2(
interp: *mut c_void,
part1: *const c_char,
part2: *const c_char,
_flags: c_int,
) -> c_int {
entered!("tcl_UnsetVar2");
let (name, index) = name_parts(part1, part2);
note("UnsetVar2", &name);
if index.is_some() {
return TCL_ERROR;
}
let Some(shared) = shared_of(interp) else {
return TCL_ERROR;
};
if runtime::unset_global_of(&shared, &name) {
TCL_OK
} else {
TCL_ERROR
}
}
pub(super) fn set_scalar(
interp: *mut c_void,
name: &str,
value: Value,
flags: c_int,
) -> *mut TclObj {
let Some(shared) = shared_of(interp) else {
return std::ptr::null_mut();
};
if let Err(message) = runtime::set_global_of(&shared, name, value) {
if flags & TCL_LEAVE_ERR_MSG != 0 {
unsafe { host::set_result_bytes(interp, message.as_bytes()) };
}
return std::ptr::null_mut();
}
let stored = runtime::global_of(&shared, name).unwrap_or(Value::Str(Arc::new(String::new())));
unsafe { cached_obj(interp, name, &stored) }
}
pub(super) unsafe fn cached_obj(interp: *mut c_void, name: &str, value: &Value) -> *mut TclObj {
let fresh = obj::from_value(value);
obj::incr_ref(fresh);
let h = &mut *(*(interp as *mut HostInterp)).host;
match h
.vars
.iter_mut()
.find(|(n, i, _)| n == name && i.is_empty())
{
Some(entry) => {
let old = std::mem::replace(&mut entry.2, fresh);
obj::release(old);
}
None => h.vars.push((name.to_string(), String::new(), fresh)),
}
fresh
}
pub(super) fn shared_of(interp: *mut c_void) -> Option<runtime::Shared> {
let host_ptr = unsafe { interp::host_of(interp) };
if host_ptr.is_null() {
return None;
}
Some(interp::shared_for(host_ptr))
}
unsafe fn c_string(p: *const c_char) -> String {
if p.is_null() {
return String::new();
}
String::from_utf8_lossy(CStr::from_ptr(p).to_bytes()).into_owned()
}
unsafe fn name_parts(part1: *const c_char, part2: *const c_char) -> (String, Option<String>) {
let name = c_string(part1);
if part2.is_null() {
if let Some(open) = name.find('(') {
if name.ends_with(')') {
let index = name[open + 1..name.len() - 1].to_string();
return (name[..open].to_string(), Some(index));
}
}
return (name, None);
}
(name, Some(c_string(part2)))
}
pub unsafe fn install_impls(t: &mut TclStubs) -> Vec<usize> {
install_sink();
vec![
install(t, "tcl_TraceVar2", trace_var2 as *const ()),
install(t, "tcl_UntraceVar2", untrace_var2 as *const ()),
install(t, "tcl_VarTraceInfo2", var_trace_info2 as *const ()),
install(t, "tcl_UnlinkVar", unlink_var as *const ()),
install(t, "tcl_UpdateLinkedVar", update_linked_var as *const ()),
install(t, "tcl_ObjGetVar2", obj_get_var2 as *const ()),
install(t, "tcl_ObjSetVar2", obj_set_var2 as *const ()),
install(t, "tcl_SetVar2Ex", set_var2_ex as *const ()),
install(t, "tcl_UnsetVar2", unset_var2 as *const ()),
]
}
unsafe fn install(t: &mut TclStubs, name: &str, f: *const ()) -> usize {
let i = TCL_NAMES
.iter()
.position(|n| *n == name)
.unwrap_or_else(|| panic!("no slot named {name} in TclStubs"));
t.slots[i] = std::mem::transmute::<*const (), RawStub>(f);
i
}