Tuesday | 30 APR 2024
[ previous ]
[ next ]

Big Integer in Zig

Title:
Date: 2023-10-25
Tags:  zig

Previously on Dragon Ball Z, ahem, I mean in my quest to start working on ScarletDME with Zig I wanted to port my BigInt/BigNum/BigWhatever logic to use the zig standard library.

I previously got op_sadd working where I had scarletdme calling my zig function. This however was very much a test and wasn't even close to working.

While trying to get this one function working I actually learned quite a bit about zig and how hard it is to try and use a language that has no docs and more importantly isn't friendly to someone who doesn't already have a rough idea of what they want to do. I'm curious how a beginner would use/learn zig because I don't see any real onramp. Ziglings and Ziglearn are fine resources but I want tutorials and stackoverflow answers. I also want examples that aren't just searching github. The struggle is real but I am also enjoying it.

Learning to Add

This is going to be some disparate thoughts but the core is that I found an example of the zig-bn library which was made part of the zig standard library, however the example I was reading was out of date and so I had some trouble reading the source code and trying to get everything to mesh. I wanted to first write a test zig program that would just use big ints and add them up. I wasn't too worried yet about actually trying to incorporate it into scarletdme.

The example in question that I was working off of:

zig-bn Github

This is quite out of date.

The current code as of 25 OCT 2023:

const std = @import("std");

const Managed = std.math.big.int.Managed;

pub fn main() !void {
   var gpa = std.heap.GeneralPurposeAllocator(.{}){};
   var allocator = gpa.allocator();
   
   var a = try Managed.init(allocator);
   var b = try Managed.init(allocator);
   
   try a.set(1990273423429836742364234234234);
   try b.set(1990273423429836742364234234234);
   
   try a.add(&a, &b);
   std.debug.print("{any}\n", .{a});
   
   try a.mul(&a, &b);
   std.debug.print("{any}\n", .{a});
}

You can run this program without building it:

zig run main.zig

This should print:

3980546846859673484728468468468
7922376600022244436595253578743342006465362857288056755133512

Now this program and the one on the github isn't drastically different but the small differences are a pain because of my own lack of knowledge. It doesn't help that I haven't really learned zig and I'm muddling through right now.

Adding it All In

Once I had a working add function, I started to work on getting ScarletDME to use my addition. This was the most painful part as even though zig and C are supposed to work together quite well, I still had issues at the boundary points.

The biggest issue was how strings work in the two languages. In C, a string is a a null terminated array. Zig has slices where it is a pointer and a length. It also does have null terminated strings but I couldn't figure out why some things worked and some things didn't. I'm going to post the code here without the error handling as it ends up being way longer with that. I'll put the trys in but really it is all catch blocks that properly pass the error back up to ScarletDME.

const std = @import("std");

const Managed = std.math.big.int.Managed;

const qm = @cImport({
    @cInclude("qm.h");
});

export fn op_sadd() void {
    var ok: bool = undefined;
    
    var s1: [1025]u8 = std.mem.zeroes([1025:0]u8);
    var s2: [1025]u8 = std.mem.zeroes([1025:0]u8);
    
    const arg2 = qm.e_stack - 1;
    ok = qm.k_get_c_string(arg2, &s2, 1024) > 0;
    qm.k_dismiss();
    
    if (!ok) {
        qm.process.status = 2;
        return;
    }
    
    const arg1 = qm.e_stack - 1;
    ok = qm.k_get_c_string(arg1, &s1, 1024) > 0;
    qm.k_dismiss();
    
    if (!ok) {
        qm.process.status = 2;
        return;
    }
    
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    var allocator = gpa.allocator(); 
    
    var a = Managed.init(allocator) catch | err | {}
    defer a.deinit();
    
    var b = Managed.init(allocator) catch | err | {}
    defer b.deinit();
    
    a.setString(10,std.mem.sliceTo(&s1,0)) catch | err | {}
    
    b.setString(10,std.mem.sliceTo(&s2,0)) catch | err | {}
    
    a.add(&a, &b) catch | err | {
    
    const ans = a.toString(allocator,10,.lower) catch | err | {}
    
    const c_str = allocator.alloc(u8,ans.len+1) catch | err | {}
    
    defer allocator.free(c_str);
    
    @memset(c_str,0);
    @memcpy(c_str[0..ans.len],ans[0..]);
    
    const ret: [*c]const u8 = &c_str[0];
    
    qm.process.status = 0;
    qm.k_put_c_string(ret, qm.e_stack);
    qm.e_stack = qm.e_stack + 1;
    
    return;
}

Pennies

The first thing is the definations of s1. This sets what I think is a string buffer that I make 1025 characters and I zero it out. I wish there was an escape character for 0 but I'm guessing this is difficult on purpose.

The next thing is that k_dismiss function. In the ScarletDME side, that function is really a define that does a stack pop using the ++ or --. This type of movement isn't allowed in zig and so I had to rewrite the k_dismiss function so that it did it in such a way that zig would compile it properly. Zig would compile the -- but it left it as an error.

The k_get_c_string worked out of the box which was nice but then when I went to use it in the big int functions of setString, it blew up on me with an invalid character error. This didn't make much sense to me but I'm sure it's by design. Zig considers the entire 1025 character array as the string and so the bigint function doesn't seem to know what to do with it. I had to call sliceTo to force the character array to be truncated so that the setString function accepted it.

A weird thing is the .lower in the toString function. I get why it's there as changing the number to a string requires the casing to be known. This is because the big int functions work on different bases. Just strange. I'm curiouse what .lower really means and what the . is really doing in general. The @ I can understand as referencing a builtin but the . seems to be doing something similar as well.

The last thing that was a pain to deal with was trying to pass the answer properly back to ScarletDME. I needed to pass back a string with a null character and so I had create one out of the zig string. This doesn't look right as it involves calling the allocator, setting the bytes and then copying the zig string to the chunk of memory I just set up. I could make this a function but then I also need to handle the freeing of that memory.

This feels very much like something that should be built into the language and I'm pretty sure I'm doing someting wrong.

That pretty much was all the issues I ran into, this was largely just groping around the dark and involved looking at the zig source code and the documentation which wasn't that great. I did realize while reading that all the string handling is in std.mem which I guess makes sense but was unexpected.

A future goal is to write up a list of the all zig functions from std.mem, fs and os areas that might be useful. This will be helpful in doing things with zig even if I don't learn zig properly.

On a tangent, this hasn't filled me with confidence that using zig is worth it. I'm not sure anymore why exactly I want to use zig. I think learning a new language is fun but this is quite troublesome. I think I'll truck ahead with porting things to zig and we'll see how implementing something from scratch goes. Hopefully if I get better then it will start feeling better.

I forgot to mention that the error handling is a shitshow right now. It looks like garbage and I'm taking that to mean that I'm doing it wrong. I do like the try and catch blocks a lot so I hope that there is a way to use it without the error code infecting everything like it has above.