Functions & Scope
Declaring functions, passing parameters by value and reference, prototypes, and how scope works in MEX
Once your script outgrows a single main(), you start breaking it into
functions. A function that draws the header. A function that formats a
profile row. A function that validates input. This is how BBS scripts stay
readable at 2am — and how you reuse code across different scripts by
putting shared functions in their own .mh header files.
MEX functions work like C functions with one important addition borrowed
from Pascal: the ref keyword for pass-by-reference parameters. If you
need a function to modify a variable in the caller’s scope — or if you’re
passing a struct to an intrinsic that fills it in — ref is how you do it.
On This Page
- Quick Reference
- Declaring Functions
- Parameters
- Pass by Reference (
ref) - Return Values
- Forward Declarations (Prototypes)
- The
main()Function - Scope Rules
- Recursive Functions
Quick Reference
| Concept | Syntax | Notes |
|---|---|---|
| Function definition | void greet() { ... } |
Body in braces |
| Typed parameters | void greet(string: name) |
Type-colon-name, comma-separated |
| Pass by reference | void fill(ref int: x) |
Caller’s variable is modified |
| Return value | int add(int: a, int: b) { return a + b; } |
Return type before name |
| Prototype | int add(int: a, int: b); |
Semicolon instead of body |
| Entry point | void main() |
Required — execution starts here |
Declaring Functions
A function has a return type, a name, a parameter list, and a body:
void draw_header()
{
ui_fill_rect(1, 1, 80, 1, ' ', UI_CYAN);
ui_write_padded(1, 1, 80, "[ My BBS ]", UI_CYAN);
}
If the function doesn’t return anything, use void. If it does, specify the
return type:
int double_it(int: x)
{
return x * 2;
}
Functions must be defined before they’re called — the compiler reads
top-to-bottom. If you have mutual recursion or just prefer putting main()
at the top, use a forward declaration.
Parameters
Parameters use the same type: name syntax as variable declarations:
void greet(string: name, int: times_called)
{
print("Welcome back, " + name + "!\n");
print("This is call #" + itostr(times_called) + ".\n");
}
Call it with matching arguments:
greet(usr.name, usr.times);
By default, parameters are passed by value. The function gets a copy. Modifying a parameter inside the function does not affect the caller’s variable:
void try_to_change(int: x)
{
x := 99; // only changes the local copy
}
void main()
{
int: n;
n := 42;
try_to_change(n);
print(itostr(n) + "\n"); // still prints 42
}
Pass by Reference (ref)
Add ref before the parameter type to pass by reference. The function
receives the actual variable, not a copy — changes inside the function
are visible to the caller:
void swap(ref int: a, ref int: b)
{
int: temp;
temp := a;
a := b;
b := temp;
}
void main()
{
int: x, y;
x := 10;
y := 20;
swap(x, y);
// x is now 20, y is now 10
}
When You Need ref
- Returning multiple values — MEX functions can only return one value.
Use
refparameters for additional outputs. -
Filling in structs — the UI style-default functions all take
refparameters:struct ui_lightbar_style: ls; ui_lightbar_style_default(ls); // fills in ls with defaults - Intrinsics that modify their arguments — functions like
timestamp(),userfindopen(), andfilefindfirst()all userefto write results back through their parameters.
Arrays Are Always ref
When you pass an array to a function, it’s always by reference — there’s no
way to pass an array by value. The function declaration uses ref array:
void fill_menu(ref array [1..] of string: items)
{
items[1] := "Read Messages";
items[2] := "File Areas";
items[3] := "Goodbye";
}
The [1..] syntax means “array starting at index 1, size determined by the
caller.” This lets you write functions that accept arrays of any size.
Return Values
Use return to exit a function and (optionally) send a value back to the
caller:
int max(int: a, int: b)
{
if (a > b)
return a;
return b;
}
A void function can use bare return; to exit early:
void maybe_greet(int: should_greet)
{
if (not should_greet)
return;
print("Hello!\n");
}
If a non-void function falls off the end without a return, the return
value is undefined. Always make sure every code path returns a value.
Forward Declarations (Prototypes)
A prototype declares a function’s signature without its body — just a semicolon where the braces would be:
// Prototypes (at top of file)
void draw_header();
void draw_footer();
int show_menu();
// Now main() can call them even though they're defined below
void main()
{
draw_header();
int: choice;
choice := show_menu();
draw_footer();
}
// Definitions follow...
void draw_header()
{
// ...
}
Prototypes are also how the .mh header files work — max.mh and
maxui.mh are full of prototypes that tell the compiler “this function
exists, here’s its signature, the runtime will provide the implementation.”
Include Files
You can put your own prototypes (and shared struct definitions, constants,
and utility functions) in a .mh file and #include it:
// mylib.mh
int clamp(int: val, int: lo, int: hi);
// myscript.mex
#include <max.mh>
#include "mylib.mh"
void main()
{
int: x;
x := clamp(usr.times, 0, 100);
}
Use angle brackets (<max.mh>) for system headers and double quotes
("mylib.mh") for your own files in the same directory.
The main() Function
Every MEX script must have a main() function. It’s the entry point —
when Maximus runs your .vm file, execution begins at main():
#include <max.mh>
void main()
{
print("Hello, " + usr.name + "!\n");
}
main() is always void and takes no parameters. When main() returns
(or execution falls off the end), the script exits and control returns to
Maximus.
Scope Rules
Global Scope
Variables declared outside any function are global — visible to every function in the file, from the point of declaration onward:
int: total_score; // global
void add_points(int: pts)
{
total_score := total_score + pts; // can access global
}
The globals exported by max.mh (usr, marea, farea, msg, sys,
id, input) are always in scope after #include <max.mh>.
Local Scope
Variables declared inside a function (or inside a block) are local to that function or block:
void example()
{
int: x; // local to example()
x := 42;
if (x > 10)
{
string: temp; // local to this block
temp := "big";
}
// temp does not exist here
}
Local variables shadow globals with the same name — the local wins inside its scope:
int: count; // global
void reset()
{
int: count; // local — shadows global
count := 0; // modifies local, not global
}
Static Data
If you need data to persist across multiple calls to a script (or across different scripts), MEX provides static data functions:
// Store a value that survives after the script exits
create_static_string("my_key");
set_static_string("my_key", "some value");
// Later (even in a different script invocation)
string: saved;
get_static_string("my_key", saved);
See Display & I/O Intrinsics for the full static data API.
Recursive Functions
MEX supports recursion — a function can call itself. The classic example:
int factorial(int: n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
Be mindful of stack depth. The MEX VM has a fixed stack size (configurable
with the -s compiler flag, default is modest). Deep recursion will
overflow it. For most BBS scripting tasks, iteration (while/for) is
the better choice.
See Also
- Variables & Types — the types your parameters and return values can have
- Control Flow —
if,while,for, andgoto - MEX Compiler — stack size flags and include path behavior