Home

Ahead of Time (AOT) Compiling of ZealC

Yep I baited you with the blog title on the home page and this is really about ZealC. Before you click away ZealC and HolyC are very similar, to the point where, for the purposes of this discussion they're the same. There is virtually no existing documentation on how to AOT compile ZealC. I think this is largely a result of the fact that

> Generally Terry discouraged usage of the AOT compiling for TempleOS because it hides the source code unlike JIT

That might be true but there are definitely reasons where compiling ahead of time is useful. I was personally trying to write a CTF challenge in an uncommon executable file format that wouldn't decompile nicely in common reverse engineering tools. When I asked how AOT compiling works and if any documentation exists Tom (a ZealOS maintainer) said

> I don't think there is any formalized documentation on that yet, it starts with a .PRJ file I think, if you look at how Compiler and Kernel are compiled it might help to see how their PRJ, HH, and ZC are all organized

So I did just that. I looked at the Compiler and Kernel source code and tried to figure out how to compile a ZealC file ahead of time. The following article is more or less what I found after some trial and error.

Assumptions

I'm going to assume you have a working knowledge of ZealC and know enough to have already written a ZealC file that presumably runs in the JIT (just in time) compiler mode. You also know enough about ZealOS to know how to find files and references.

The Build System

Generally the Compiler starts by reading in a .PRJ file which more or less describes where to find declarations for all of the relevant external symbols that aren't defined in your ZealC files.

            
                I64 Comp(U8 *filename, U8 *map_name=NULL, U8 *out_name=NULL, U8 mapfile_drive_let=0)
                    {//AOT Compile ZC or PRJ file and output ZXE file. Returns err_count.
            
        

You're going to minimally want the following definitions in you .PRJ file:

            
                #exe {
                    Cd(__DIR__);;
                    Option(OPTf_WARN_PAREN,		ON);
                    Option(OPTf_WARN_DUP_TYPES,	ON);
                    Option(OPTf_KEEP_PRIVATE,	ON);
                }
                #include "/Kernel/KernelA.HH"
                #include "/Compiler/CompilerA.HH"
                #exe {Option(OPTf_EXTERNS_TO_IMPORTS,	ON);};
                #include "/Kernel/KernelB.HH"
                #include "/Kernel/KernelC.HH"
                #include "/Compiler/CompilerB.HH"
            
        

Without this the compiler will be unable to resolve anything. It won't even know what basic types like U8 are.

Compiling

Now that you have a .PRJ file with the minimal ZealC headers included you can start including and declaring the relevant symbols for your project. It's not uncommon that you'll have to write headers for kernel interfaces like BST.HH and Socket.HH. In my case I'm doing TCP networking so I also need to include TCP.HH.

            
                #include "/Home/Sockets.HH"
                #include "/Home/BST.HH"
                #include "/Home/Net/Protocols/TCP/TCP.HH"
            
        
These will typically just include object definitions and function prototypes. For example I had to tear out the abstract class definition for sockets and some defines because they were defined in the relevant ZealC file and I didn't feel like including unnecessary source function definitions in my ZXE and then externing all of the ZealC functions used in Sockets.ZC. Hence the existence of /Home/Sockets.HH which looks something like:
            
                #define SOCKET_STATE_READY          0
                #define SOCKET_STATE_BIND_REQ       1
                #define SOCKET_STATE_CONNECT_REQ    2
                ...
                class CSocket
                {
                    U8	state;

                    U16	type;
                    U16 domain;
                };
            
        

Next you're going to have to declare any functions not defined in your ZealC files as extern. There are some header files like /Compiler/CExterns.ZC that declare a lot of common functions as extern. Unfortunately this doesn't include most functions throughout the ecosystem which means you're going to have to hunt them down so that you can use the prototype to declare them. You could probably define these in a header file if they become to numerous but in my case I felt it was fine to inline them in the .PRJ file. This ends up looking something like:

            
                extern CTCPSocket TCPSocket(U16 domain=AF_UNSPEC);
                extern I64 TCPSocketBind(CTCPSocket *tcp_socket, CSocketAddressStorage *address);
                extern I64 TCPSocketListen(CTCPSocket *tcp_socket, I64 backlog_size);
                extern CTCPSocket *TCPSocketAccept(CTCPSocket *tcp_socket);
                extern I64 TCPSocketReceive(CTCPSocket *tcp_socket, U8 *buffer, I64 length);
                extern CDC *DCAlias(CDC *dc=NULL, CTask *task=NULL);
                extern Bool GrLine(CDC *dc, I64 x1, I64 y1, I64 x2, I64 y2, I64 step=1, I64 start=0);
                extern U0 DCFill(CDC *dc=NULL, CColorROPU32 val=TRANSPARENT);
                extern U0 DCDel(CDC *dc);
            
        

Finally you'll want to include your actual ZealC source files. As long as you have forward declarations you should be able to include them in any order.

            
                #include "chal"
            
        
And that's how UnholyEXE was compiled for WolvCTF 2024. Source code will be available on WolvSec's github after the CTF is over. I hope this helps some other lost souls out there trying to compile ZealC/HolyC files in AOT mode.