Using Zig's translate-C to understand weird C code
zig.news
zig.news
void f(int * a)
{
void * p = reinterpret_cast<void *>(&a);
***reinterpret_cast<int *(*)[]>(p) = 1;
}(not sure what steps that should be)
It's a blog post hosted on zig.news (so intended for an audience that knows Zig), I think we can safely assume that it's not meant to be an universal guide for understanding C code, but rather the recount of a (somewhat minor) experience lived by the author. It's even stated upfront:
> @rep_stosq_void on Twitter posted this strange sample of C code, and I wanted to show my process of understanding this contrived C code.
"I was presented with this code, and this is what I did to understand it"
I've happened to have written a lot of C, but some areas (like this one) I'm not as confident in. This way of casting, and working with types is very poor syntax in my opinion (I mean below in this thread you have people arguing about the spiral rule and such, it's obviously a common confusion).
Zig's way of expressing types and pointers is far superior, these transformations I made were done quickly and I didn't feel like there was any ambiguity or confusion in anything. Just a series of simple reductions, there are no "tricks" or easy mistakes to make in the code. It feels like a trivial proof.
Obviously I am biased, this was posted to zig.news. I thought it was a neat showcase of translate-c, and how zig does some of these things nicer. I'm not telling everyone that they should do what I did, but this works for me and I'm happy to share.
My intuition (because I don't usually have to deal with this kind of nonsense) is "cast to a pointer to an array of int pointers". cdecl confirms that: https://cdecl.org/?q=int*%28*x%29%5B%5D
So we cast a (pointer to pointer to int) to (pointer to array of int pointers) [we can ignore the detour through void*] and then immediately dereference through all three layers. Which gives us back the only int in the program.
Excellent example of how to be a Three Star Programmer I guess: https://wiki.c2.com/?ThreeStarProgrammer
I don't code in C day to day but this will help for the odd time I need to understand C code!
Interpret p as a pointer to an array of pointers to integer. Since arrays decay to pointers, you triple dereference it to get
1. the array
2. the first element of that array
3. whatever int that element is pointing to
Then set it to 1.
It took me about 5 seconds. I have no idea what that Zig is supposed to do on the other hand.
Note: the programmer who wrote that C code was being very obtuse. This is essentially just `*a = 1`, the whole p thing is some form of baroque code.
Perhaps, but the spiral rule is one of the most complicated parts of C. Most things are trivial to someone who knows how to do the thing.
* the tricky part is that `()` are also used to denote functions. So yeah, it's not always readable. `(*)()` would be a pointer to a function returning int (the default type) and taking an unspecified amount of arguments.
The spiral rule works only if there is no pointer to pointer or array of array in the type. But take this for example:
+----------------------------+
| +-----------------------+ |
| | +------------------+ | |
| | | +-------------+ | | |
| | | | +--------+ | | | |
| | | | | +--+ | | | | |
| | | | | ^ | | | | | |
int * * ¦ ¦ ¦ xxx[1][2][3] | | |
^ | | | | | | | | | | |
| | | | | +-----+ | | | | |
| | | | +----------+ | | | |
| | | +---------------+ | | |
| | ---------------------+ | |
| +-------------------------+ |
+-------------------------------+
The type of xxx is a [1-element] array of [2-element] array of [3-element] array of pointer to pointer to ints. I drew a spiral that passes through each specifier in the correct order.Notice that to make the spiral correct it has to skip the pointer specifiers in the first three loops. This is marked by ¦. This is not mentioned in the original spiral rules and one could be forgiven to parse the expression as xxx -> [1] -> pointer -> [2] -> etc. following a spiral that doesn't skip the pointers.
The Right-Left Rule is quoted less frequently on HN but it's a correct algorithm for deciphering C types: http://cseweb.ucsd.edu/~ricko/rt_lt.rule.html
The spiral rule can be modified to process all array specifiers before all pointer specifiers, but then you'd have to specify that the order to do so is right and then left. At that point it's just the Right-Left Rule.
int *f();
is a function returning pointer to int, while int (*f)()
is a pointer to function returning int. Likewise, int *a[]
is an array of pointer to int, while int (*a)[]
is a pointer to array of int. *(int[])"Pointer to array of int" means you dereference it first to get an array of ints, then index to get an int.
EDIT: Uh, apparently you can add parens to casts but not declarations.
This doesn't compile:
int (*) p = (int *)&i;
This does: int *p = (int (*))&i
I can't quite justify this behaviour.You can in fact have parentheses in declarations, but the identifier must be on the inside, not just to the right of everything: https://godbolt.org/z/vKzcYMdvK
int **p[123]; // p is array(123) of pointer to pointer to int
int *(*p)[123]; // p is a pointer to array(123) of pointer to int
Sometimes people find casts confusing because there is no identifier inside. But you can easily read it if you know where the identifier would be in an equivalent declaration.Precedence is usually documented in a man page called operator. http://man.openbsd.org/operator
The cast says "treat this int* as an array of int*, then the first dereference says "give me the first element of that array", which gets you back the original int*.
"We start with a pointer-to-int called "a", take pointer to it and name it "p" (it's a pointer-to-pointer-to-int, although we store it in a pointer-to-whatever variable), then cast it to some weird type, dereference it thrice and store 1 into the resulting target. Two questions remain: a) what is that weird type? b) we have two levels of indirection but three dereferences, how does it work? The answer to the first question is that weird type is "pointer-to-array-of-pointers-to-int", and it helps us to answer the second question: dereferencing a value of that type is a no-op in arithmetical sense (but has a type-casting effect)."
Also, because this array type is incomplete (it has no size), I don't think you could use it in any context where it didn't "decay" into a pointer to its first element (you can test what your compiler says if you try to measure the array's size with the sizeof operator when only using one dereference).
It's very subjective what is beautiful or ugly, of course. It'd be more interesting if you can offer specific critique rather than just calling it ugly.
You're probably not the only one, I for one would call any non-lisp "ugly", but again, highly subjective, as many others find some C-like code beautiful but other C-like code ugly.
if it was, something like trypophobia would not exist
Then using modules with JavaScript AMD pattern is also not appealing.
[0] or really any statement at all about aesthetics
[1] I know for a fact pjmlp will also complain about lack of built-in gc or ref-count pointer safety.
Semantically Zig is really interesting though.
I hate global variables so much it's one of the reasons I got rid of libc. Freestanding C turned out to be a superior language just because it lacks all the libc cruft.
I ultimately dropped Ruby because of global state. It's such a wonderful language but it has one fatal flaw: lack of proper modules. The require method just executes Ruby source files, modifying the global state of the interpreter. It ceased to be a beautiful language once I realized this. Python's modules are superior, and the Javascript approach is the best one: just a normal function that returns a normal object containing exported data and functions. Javascript modules are c.ompletely reified.
Zig has no inheritance but everything is namespaced, including declaring functions inside struct definitions so that you can use them as if they were methods.
Like unsafePerformIO in Haskell.
Reasonable Zig code looks more like this:
https://github.com/riverwm/river/blob/master/riverctl/main.z...
That said I think it's fine if you don't like the syntax. I think that some complaints are honestly too superficial to be legitimate (like complaining about builtins being prefixed with @), but at the same time Zig is often times prioritizing explicitness over "good looking".
I personally consider Swift a very good looking language, but then I look at all the new features that got added since I used it last, remember that I value simplicity over aesthetics, and go back to Zig.
I do find it practical, with an aesthetically barebones approach.
**(*(p as *mut [*mut libc::c_int; 0])).as_mut_ptr() = 1 as libc::c_int;
which is still needlessly complicated, and not even quite accurate due to giving the array a 0 size (the as_mut_ptr() converts the array back to a C pointer).It doesn't seem inaccurate to me, more like the best choice at hand. If the C array has a known length, the Rust code has it too. Only if the C code has an array of unknown length does the Rust code use a 0-length array. Furthermore, if the C code indexes the array of unknown length, the Rust code uses .as_mut_ptr().offset(...) instead of directly indexing the array. So the fact that it represents C arrays of unknown length with Rust arrays of 0 length does not cause any problem, because the generated code is consistent.
char foo(void* p) {
char (*arr)[] = (char (*)[])p;
return (*arr)[1];
}
char bar(void* p) {
char (*arr)[3] = (char (*)[3])p;
return (*arr)[1];
}
... translates to: pub unsafe extern "C" fn foo(mut p: *mut libc::c_void) -> libc::c_char {
let mut arr: *mut [libc::c_char; 0] = p as *mut [libc::c_char; 0];
return *(*arr).as_mut_ptr().offset(1 as libc::c_int as isize);
}
pub unsafe extern "C" fn bar(mut p: *mut libc::c_void) -> libc::c_char {
let mut arr: *mut [libc::c_char; 3] = p as *mut [libc::c_char; 3];
return (*arr)[1 as libc::c_int as usize];
}perl -MO=Deparse /some/script
Outputs a (usually) more readable equivalent, and also works for one-liners that use -e "somesnippet".
void f(int *a) {
int **p = &a;
**p = 1; // the same as *a = 1;
}
Making a concise explanation of how exactly the third dereferencing disappeared is left as a further exercise for the reader.In the standard at https://web.archive.org/web/20181230041359if_/http://www.ope...
6.5.3.2 Address and indirection operators
Constraints
1 The operand of the unary & operator shall be either a
function designator, the result of a [] or unary
\* operator, or an lvalue that designates an object that
is not a bit-field and is not declared with the
register storage-class specifier.
So as the parameter is an lvalue it is guaranteed to work with the & operator.