>
I'm not sure I understand why format is a macro and print and equivalent in most other programming languages is a regular function. There's some complexity there that I guess I just don't know.It's a really good question! It's a little bit complex but hopefully not too much, and it shines a light on some interesting differences between languages.
The answer is roughly that Rust is strongly typed, and println!, format!, etc. all accept arbitrary types. They all also accept a varying number of arguments, and Rust only allows you to define functions with a fixed number of arguments. Python, sh, etc. have neither of these restrictions.
What would the type (function signature) of println/format/etc. be if they were regular functions?
fn println(format: &str, arg1: SomeType?, arg2??);
You could solve the variable-number-of-arguments problem in Rust itself somehow, maybe
fn println(format: &str, args...: SomeType);
but you'd have a few problems remaining.
First, you'd need to figure out what that type is. The formatting macros don't just accept things that implement Display - they also accept things that implement Debug if you use {:?}, things that implement LowerHex if you use {:x}, etc. And the macro wants to return a compile-time error if you mismatch types and the formatting string, like
println!("Hex {:x}, debug {:?}", [1, 2, 3], 4);
And second, and a little more simply, the macro wants to return a compile-time error if you just have the wrong number of things being formatted:
println!("Three things: {} {} {}", 1, 2);
In order to do that level of checking based on the value of the string, it has to be some sort of compile-time extension. In Python, you'd just get a runtime error and in bash, you'd just get empty strings.
>>> "{:x}".format("hi")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Unknown format code 'x' for object of type 'str'
>>> "{} {} {}".format(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: tuple index out of range
Regular functions in Rust (and any other language) don't get to see their arguments
at compile time; they only see their arguments when they're called.
C, of course, just ignores this problem entirely. You don't get an error if you mess up the format string; you just get invalid reads to memory.
printf("Is this a pointer? %s", 1234);
printf("What's the third thing? %d %d %d", 1, 2);
Also (especially) in C, it's very easy to accidentally let the format string be input external to your program:
printf(username);
which means that an attacker can specify the format string and cause reads to memory you weren't expecting and cause data to be leaked (or worse, an attacker can cause
writes: C's %m token
stores the number of bytes written to the next argument to printf, interpreted as a pointer - or the next thing that happens to be memory, if you didn't pass an argument for it). Rust protects against this by making you use a string literal as the first argument.
Python also protects against this by having f-strings be a special part of the language syntax. You can't have an f-string that's a variable, so an attacker can't set their username to f"{DATABASE_PASSWORD}". So, in that sense, the f sigil in Python is also a macro, like in Rust - it's just shorter syntax.
It's a really good question! It's a little bit complex but hopefully not too much, and it shines a light on some interesting differences between languages.
The answer is roughly that Rust is strongly typed, and println!, format!, etc. all accept arbitrary types. They all also accept a varying number of arguments, and Rust only allows you to define functions with a fixed number of arguments. Python, sh, etc. have neither of these restrictions.
What would the type (function signature) of println/format/etc. be if they were regular functions?
You could solve the variable-number-of-arguments problem in Rust itself somehow, maybe but you'd have a few problems remaining.First, you'd need to figure out what that type is. The formatting macros don't just accept things that implement Display - they also accept things that implement Debug if you use {:?}, things that implement LowerHex if you use {:x}, etc. And the macro wants to return a compile-time error if you mismatch types and the formatting string, like
And second, and a little more simply, the macro wants to return a compile-time error if you just have the wrong number of things being formatted: In order to do that level of checking based on the value of the string, it has to be some sort of compile-time extension. In Python, you'd just get a runtime error and in bash, you'd just get empty strings. Regular functions in Rust (and any other language) don't get to see their arguments at compile time; they only see their arguments when they're called.C, of course, just ignores this problem entirely. You don't get an error if you mess up the format string; you just get invalid reads to memory.
Also (especially) in C, it's very easy to accidentally let the format string be input external to your program: which means that an attacker can specify the format string and cause reads to memory you weren't expecting and cause data to be leaked (or worse, an attacker can cause writes: C's %m token stores the number of bytes written to the next argument to printf, interpreted as a pointer - or the next thing that happens to be memory, if you didn't pass an argument for it). Rust protects against this by making you use a string literal as the first argument.Python also protects against this by having f-strings be a special part of the language syntax. You can't have an f-string that's a variable, so an attacker can't set their username to f"{DATABASE_PASSWORD}". So, in that sense, the f sigil in Python is also a macro, like in Rust - it's just shorter syntax.