use crate::atom::{Atom, AtomTable};
use crate::native::{
BifRegistryImpl, Capability, NativeFn, NativeRegistrationError, ProcessContext,
};
use crate::process::Priority;
use crate::term::Term;
const SUPPORTED_ITEMS: &[ProcessInfoItem] = &[
ProcessInfoItem::CurrentFunction,
ProcessInfoItem::HeapSize,
ProcessInfoItem::MessageQueueLen,
ProcessInfoItem::RegisteredName,
ProcessInfoItem::Status,
ProcessInfoItem::TrapExit,
ProcessInfoItem::Priority,
ProcessInfoItem::Links,
ProcessInfoItem::Monitors,
];
type ProcessInfoBif = (&'static str, u8, Capability, NativeFn);
const PROCESS_INFO_BIFS: &[ProcessInfoBif] = &[
("process_info", 1, Capability::Pure, bif_process_info_1),
("process_info", 2, Capability::Pure, bif_process_info_2),
(
"group_leader",
0,
Capability::ProcessLocal,
bif_group_leader_0,
),
(
"group_leader",
2,
Capability::ProcessLocal,
bif_group_leader_2,
),
];
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ProcessInfoItem {
CurrentFunction,
HeapSize,
MessageQueueLen,
RegisteredName,
Status,
TrapExit,
Priority,
Links,
Monitors,
}
fn priority_atom(context: &ProcessContext, priority: Priority) -> Result<Atom, Term> {
let atom_table = context.atom_table().ok_or_else(badarg)?;
Ok(match priority {
Priority::Low => atom_table.intern("low"),
Priority::Normal => Atom::NORMAL,
Priority::High => atom_table.intern("high"),
Priority::Max => atom_table.intern("max"),
})
}
impl ProcessInfoItem {
fn name(self) -> &'static str {
match self {
Self::CurrentFunction => "current_function",
Self::HeapSize => "heap_size",
Self::MessageQueueLen => "message_queue_len",
Self::RegisteredName => "registered_name",
Self::Status => "status",
Self::TrapExit => "trap_exit",
Self::Priority => "priority",
Self::Links => "links",
Self::Monitors => "monitors",
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ProcessInfoStatus {
Running,
Waiting,
Suspended,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ProcessMonitorInfo {
pub watcher: u64,
pub target: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProcessInfoValue {
CurrentFunction(Option<(Atom, Atom, u8)>),
HeapSize(usize),
MessageQueueLen(usize),
RegisteredName(Option<Atom>),
Status(ProcessInfoStatus),
TrapExit(bool),
Priority(Priority),
Links(Vec<u64>),
Monitors(Vec<ProcessMonitorInfo>),
}
pub trait ProcessInfoFacility: Send + Sync {
fn process_info(&self, pid: u64, item: ProcessInfoItem) -> Option<ProcessInfoValue>;
}
pub fn register_process_info_bifs(
registry: &BifRegistryImpl,
atom_table: &AtomTable,
) -> Result<(), NativeRegistrationError> {
let erlang = atom_table.intern("erlang");
for &(function_name, arity, capability, native_function) in PROCESS_INFO_BIFS {
let function = atom_table.intern(function_name);
registry.register(erlang, function, arity, native_function, capability)?;
}
Ok(())
}
pub fn bif_process_info_2(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [pid_term, item_term] = args else {
return Err(badarg());
};
let pid = pid_term.as_pid().ok_or_else(badarg)?;
let item_atom = item_term.as_atom().ok_or_else(badarg)?;
let item = parse_item(context, item_atom)?;
let Some(value) = query_process_info(context, pid, item)? else {
return Ok(Term::atom(Atom::UNDEFINED));
};
let words = 3 + value_heap_words(pid, &value);
context.ensure_heap_space(words)?;
let value_term = alloc_value(context, pid, value)?;
context.alloc_tuple(&[*item_term, value_term])
}
pub fn bif_process_info_1(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [pid_term] = args else {
return Err(badarg());
};
let pid = pid_term.as_pid().ok_or_else(badarg)?;
let mut values = Vec::with_capacity(SUPPORTED_ITEMS.len());
for item in SUPPORTED_ITEMS {
let Some(value) = query_process_info(context, pid, *item)? else {
return Ok(Term::atom(Atom::UNDEFINED));
};
values.push((*item, value));
}
let words = values
.iter()
.map(|(_, value)| 3 + value_heap_words(pid, value) + 2)
.sum();
context.ensure_heap_space(words)?;
let mut tuples = Vec::with_capacity(values.len());
for (item, value) in values {
let item_atom = intern_item_atom(context, item)?;
let value_term = alloc_value(context, pid, value)?;
tuples.push(context.alloc_tuple(&[Term::atom(item_atom), value_term])?);
}
context.alloc_list(&tuples)
}
fn query_process_info(
context: &ProcessContext,
pid: u64,
item: ProcessInfoItem,
) -> Result<Option<ProcessInfoValue>, Term> {
if item == ProcessInfoItem::Priority
&& context.pid() == Some(pid)
&& let Ok(priority) = context.priority()
{
return Ok(Some(ProcessInfoValue::Priority(priority)));
}
let facility = context.process_info_facility().ok_or_else(badarg)?;
Ok(facility.process_info(pid, item))
}
fn parse_item(context: &ProcessContext, atom: Atom) -> Result<ProcessInfoItem, Term> {
for item in SUPPORTED_ITEMS {
if intern_item_atom(context, *item)? == atom {
return Ok(*item);
}
}
Err(badarg())
}
fn intern_item_atom(context: &ProcessContext, item: ProcessInfoItem) -> Result<Atom, Term> {
let atom_table = context.atom_table().ok_or_else(badarg)?;
Ok(atom_table.intern(item.name()))
}
fn alloc_value(
context: &mut ProcessContext,
queried_pid: u64,
value: ProcessInfoValue,
) -> Result<Term, Term> {
match value {
ProcessInfoValue::CurrentFunction(current_mfa) => {
let (module, function, arity) =
current_mfa.unwrap_or((Atom::UNDEFINED, Atom::UNDEFINED, 0));
context.alloc_tuple(&[
Term::atom(module),
Term::atom(function),
Term::small_int(i64::from(arity)),
])
}
ProcessInfoValue::HeapSize(words) | ProcessInfoValue::MessageQueueLen(words) => {
usize_to_small_int(words)
}
ProcessInfoValue::RegisteredName(Some(name)) => Ok(Term::atom(name)),
ProcessInfoValue::RegisteredName(None) => Ok(Term::NIL),
ProcessInfoValue::Status(status) => Ok(Term::atom(status_atom(context, status)?)),
ProcessInfoValue::TrapExit(value) => Ok(bool_to_atom(value)),
ProcessInfoValue::Priority(priority) => Ok(Term::atom(priority_atom(context, priority)?)),
ProcessInfoValue::Links(links) => alloc_pid_list(context, &links),
ProcessInfoValue::Monitors(monitors) => alloc_monitor_list(context, queried_pid, &monitors),
}
}
fn value_heap_words(queried_pid: u64, value: &ProcessInfoValue) -> usize {
match value {
ProcessInfoValue::CurrentFunction(_) => 4,
ProcessInfoValue::HeapSize(_)
| ProcessInfoValue::MessageQueueLen(_)
| ProcessInfoValue::RegisteredName(_)
| ProcessInfoValue::Status(_)
| ProcessInfoValue::Priority(_)
| ProcessInfoValue::TrapExit(_) => 0,
ProcessInfoValue::Links(links) => links.len() * 2,
ProcessInfoValue::Monitors(monitors) => {
monitors
.iter()
.filter(|monitor| monitor.watcher == queried_pid)
.count()
* 4
}
}
}
fn status_atom(context: &ProcessContext, status: ProcessInfoStatus) -> Result<Atom, Term> {
let atom_table = context.atom_table().ok_or_else(badarg)?;
Ok(match status {
ProcessInfoStatus::Running => atom_table.intern("running"),
ProcessInfoStatus::Waiting => atom_table.intern("waiting"),
ProcessInfoStatus::Suspended => atom_table.intern("suspended"),
})
}
fn alloc_pid_list(context: &mut ProcessContext, pids: &[u64]) -> Result<Term, Term> {
let mut terms = Vec::with_capacity(pids.len());
for pid in pids {
terms.push(Term::try_pid(*pid).ok_or_else(badarg)?);
}
context.alloc_list(&terms)
}
fn alloc_monitor_list(
context: &mut ProcessContext,
queried_pid: u64,
monitors: &[ProcessMonitorInfo],
) -> Result<Term, Term> {
let process_atom = Term::atom(Atom::PROCESS);
let mut terms = Vec::new();
for monitor in monitors {
if monitor.watcher == queried_pid {
let target = Term::try_pid(monitor.target).ok_or_else(badarg)?;
terms.push(context.alloc_tuple(&[process_atom, target])?);
}
}
context.alloc_list(&terms)
}
fn usize_to_small_int(value: usize) -> Result<Term, Term> {
let value = i64::try_from(value).map_err(|_| badarg())?;
Term::try_small_int(value).ok_or_else(badarg)
}
fn bool_to_atom(value: bool) -> Term {
Term::atom(if value { Atom::TRUE } else { Atom::FALSE })
}
fn badarg() -> Term {
Term::atom(Atom::BADARG)
}
pub fn bif_group_leader_0(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
if !args.is_empty() {
return Err(badarg());
}
context.group_leader()
}
pub fn bif_group_leader_2(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
let [new_leader, pid] = args else {
return Err(badarg());
};
if !new_leader.is_pid() {
return Err(badarg());
}
let Some(target_pid) = pid.as_pid() else {
return Err(badarg());
};
let facility = context.group_leader_facility().ok_or_else(badarg)?;
facility
.set_group_leader(target_pid, *new_leader)
.map_err(|_| badarg())?;
context.set_attached_group_leader(target_pid, *new_leader);
Ok(Term::atom(Atom::TRUE))
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use super::*;
use crate::process::{Priority, Process};
use crate::term::boxed::{Cons, Tuple};
#[derive(Default)]
struct MockProcessInfoFacility {
values: Mutex<HashMap<(u64, ProcessInfoItem), ProcessInfoValue>>,
}
impl MockProcessInfoFacility {
fn insert(&self, pid: u64, item: ProcessInfoItem, value: ProcessInfoValue) {
self.values
.lock()
.expect("values mutex")
.insert((pid, item), value);
}
}
impl ProcessInfoFacility for MockProcessInfoFacility {
fn process_info(&self, pid: u64, item: ProcessInfoItem) -> Option<ProcessInfoValue> {
self.values
.lock()
.expect("values mutex")
.get(&(pid, item))
.cloned()
}
}
fn context_with_facility(
atom_table: Arc<AtomTable>,
facility: Arc<MockProcessInfoFacility>,
process: &mut Process,
) -> ProcessContext<'_> {
let mut context = ProcessContext::new();
context.set_atom_table(Some(atom_table));
context.set_process_info_facility(Some(facility));
context.attach_process(process, 0);
context
}
fn tuple_elements(term: Term) -> Vec<Term> {
let tuple = Tuple::new(term).expect("tuple term");
(0..tuple.arity())
.filter_map(|index| tuple.get(index))
.collect()
}
fn list_elements(mut term: Term) -> Vec<Term> {
let mut elements = Vec::new();
while term != Term::NIL {
let cons = Cons::new(term).expect("proper list cons");
elements.push(cons.head());
term = cons.tail();
}
elements
}
#[test]
fn register_process_info_bifs_registers_process_info_1_and_2() {
let atom_table = AtomTable::with_common_atoms();
let registry = BifRegistryImpl::new();
register_process_info_bifs(®istry, &atom_table).expect("registration");
let erlang = atom_table.intern("erlang");
let process_info = atom_table.intern("process_info");
assert!(registry.lookup(erlang, process_info, 1).is_some());
assert!(registry.lookup(erlang, process_info, 2).is_some());
}
#[test]
fn process_info_2_returns_tuple_for_each_supported_item() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockProcessInfoFacility::default());
let pid = 7;
let module = atom_table.intern("mod");
let function = atom_table.intern("fun");
facility.insert(
pid,
ProcessInfoItem::CurrentFunction,
ProcessInfoValue::CurrentFunction(Some((module, function, 2))),
);
facility.insert(
pid,
ProcessInfoItem::HeapSize,
ProcessInfoValue::HeapSize(10),
);
facility.insert(
pid,
ProcessInfoItem::MessageQueueLen,
ProcessInfoValue::MessageQueueLen(3),
);
facility.insert(
pid,
ProcessInfoItem::RegisteredName,
ProcessInfoValue::RegisteredName(Some(atom_table.intern("name"))),
);
facility.insert(
pid,
ProcessInfoItem::Status,
ProcessInfoValue::Status(ProcessInfoStatus::Running),
);
facility.insert(
pid,
ProcessInfoItem::TrapExit,
ProcessInfoValue::TrapExit(true),
);
facility.insert(
pid,
ProcessInfoItem::Priority,
ProcessInfoValue::Priority(Priority::High),
);
facility.insert(
pid,
ProcessInfoItem::Links,
ProcessInfoValue::Links(vec![1, 2]),
);
facility.insert(
pid,
ProcessInfoItem::Monitors,
ProcessInfoValue::Monitors(vec![ProcessMonitorInfo {
watcher: pid,
target: 9,
}]),
);
let item_names = [
"current_function",
"heap_size",
"message_queue_len",
"registered_name",
"status",
"trap_exit",
"priority",
"links",
"monitors",
];
for item_name in item_names {
let mut process = Process::new(0, 128);
let mut context =
context_with_facility(Arc::clone(&atom_table), Arc::clone(&facility), &mut process);
let item = atom_table.intern(item_name);
let result = bif_process_info_2(&[Term::pid(pid), Term::atom(item)], &mut context)
.expect("process_info/2 succeeds");
let elements = tuple_elements(result);
assert_eq!(elements.len(), 2);
assert_eq!(elements[0], Term::atom(item));
}
}
#[test]
fn process_info_2_returns_undefined_for_missing_process_and_badarg_for_unknown_item() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockProcessInfoFacility::default());
let mut process = Process::new(0, 128);
let mut context = context_with_facility(atom_table.clone(), facility, &mut process);
let heap_size = atom_table.intern("heap_size");
assert_eq!(
bif_process_info_2(&[Term::pid(99), Term::atom(heap_size)], &mut context),
Ok(Term::atom(Atom::UNDEFINED))
);
let unknown = atom_table.intern("unknown_process_info_item");
assert_eq!(
bif_process_info_2(&[Term::pid(99), Term::atom(unknown)], &mut context),
Err(Term::atom(Atom::BADARG))
);
}
#[test]
fn process_info_requires_configured_facility() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let mut process = Process::new(0, 128);
let mut context = ProcessContext::new();
context.set_atom_table(Some(atom_table.clone()));
context.attach_process(&mut process, 0);
let heap_size = atom_table.intern("heap_size");
assert_eq!(
bif_process_info_2(&[Term::pid(1), Term::atom(heap_size)], &mut context),
Err(Term::atom(Atom::BADARG))
);
assert_eq!(
bif_process_info_1(&[Term::pid(1)], &mut context),
Err(Term::atom(Atom::BADARG))
);
}
#[test]
fn process_info_1_returns_deterministic_list_of_all_supported_items() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockProcessInfoFacility::default());
let pid = 11;
for item in SUPPORTED_ITEMS {
let value = match item {
ProcessInfoItem::CurrentFunction => ProcessInfoValue::CurrentFunction(None),
ProcessInfoItem::HeapSize => ProcessInfoValue::HeapSize(0),
ProcessInfoItem::MessageQueueLen => ProcessInfoValue::MessageQueueLen(0),
ProcessInfoItem::RegisteredName => ProcessInfoValue::RegisteredName(None),
ProcessInfoItem::Status => ProcessInfoValue::Status(ProcessInfoStatus::Waiting),
ProcessInfoItem::TrapExit => ProcessInfoValue::TrapExit(false),
ProcessInfoItem::Priority => ProcessInfoValue::Priority(Priority::Normal),
ProcessInfoItem::Links => ProcessInfoValue::Links(Vec::new()),
ProcessInfoItem::Monitors => ProcessInfoValue::Monitors(Vec::new()),
};
facility.insert(pid, *item, value);
}
let mut process = Process::new(0, 256);
let mut context = context_with_facility(atom_table.clone(), facility, &mut process);
let result = bif_process_info_1(&[Term::pid(pid)], &mut context).expect("process_info/1");
let entries = list_elements(result);
assert_eq!(entries.len(), SUPPORTED_ITEMS.len());
for (entry, item) in entries.into_iter().zip(SUPPORTED_ITEMS) {
let tuple = tuple_elements(entry);
assert_eq!(tuple[0], Term::atom(atom_table.intern(item.name())));
}
}
#[test]
fn process_info_2_returns_priority_atom() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockProcessInfoFacility::default());
let pid = 21;
facility.insert(
pid,
ProcessInfoItem::Priority,
ProcessInfoValue::Priority(Priority::High),
);
let mut process = Process::new(0, 128);
let mut context =
context_with_facility(Arc::clone(&atom_table), Arc::clone(&facility), &mut process);
let priority = atom_table.intern("priority");
let result = bif_process_info_2(&[Term::pid(pid), Term::atom(priority)], &mut context)
.expect("process_info/2 succeeds");
let tuple = tuple_elements(result);
assert_eq!(tuple[0], Term::atom(priority));
assert_eq!(tuple[1], Term::atom(atom_table.intern("high")));
}
#[test]
fn process_info_2_reports_attached_process_priority_after_process_flag_change() {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let facility = Arc::new(MockProcessInfoFacility::default());
let pid = 21;
facility.insert(
pid,
ProcessInfoItem::Priority,
ProcessInfoValue::Priority(Priority::Normal),
);
let mut process = Process::new(pid, 128);
let mut context =
context_with_facility(Arc::clone(&atom_table), Arc::clone(&facility), &mut process);
context.set_priority(Priority::High).expect("set priority");
let priority = atom_table.intern("priority");
let result = bif_process_info_2(&[Term::pid(pid), Term::atom(priority)], &mut context)
.expect("process_info/2 succeeds");
let tuple = tuple_elements(result);
assert_eq!(tuple[0], Term::atom(priority));
assert_eq!(tuple[1], Term::atom(atom_table.intern("high")));
}
}