City Hash ᴾᴴᴾ - chung-leong/zigar GitHub Wiki

JavaScript | PHP


In this example we're going to create a function that calculate the CityHash of the given data. CityHash is a non-cryptographic algorithm developed at Google that's designed for speed. Support for it is built into Zig's standard library.

Creating the sample app

First, create the basic directory structure:

mkdir city-hash
cd city-hash
mkdir src zig

Then add hash.zig to zig:

const std = @import("std");
const CityHash64 = std.hash.cityhash.CityHash64;
pub const hash = CityHash64.hash;

Followed by hash.php in src:

<?php

$m = zigar_use(__DIR__ . '/../zig/hash.zig');

$hash = $m->hash("Hello world");
echo "$hash\n";

At this point, if you haven't install the php-zigar extension, do so. Follow the instructions in the installation guide example. When ready, run the script:

php src/hash.php

After a while, we get the result:

4018340516250275620

CityHash64.hash() returns a u64, a non-negative 64-bit integer. Because PHP only supports signed integers, results from the function can be negative sometimes. For example, if we change the input text to "Hello world!!", we would get the following:

-5363615455483602812

Returning a hex string

Hashes are often used for creating unique file names. It's more convenient to get a hex string from the function than a number. To achieve this, we need to write our own function:

const std = @import("std");
const CityHash64 = std.hash.cityhash.CityHash64;

pub fn hash(data: []const u8) ![16]u8 {
    const value = std.hash.cityhash.CityHash64.hash(data);
    var buf: [16]u8 = undefined;
    _ = try std.fmt.bufPrint(&buf, "{x}", .{value});
    return buf;
}

When we run the script, we now get the following:

37c40368d1a84f24

Suppose we had mistakenly believed that the hex string for a 64-bit number would be 8-character long:

const std = @import("std");
const CityHash64 = std.hash.cityhash.CityHash64;

// pub const hash = CityHash64.hash;

pub fn hash(data: []const u8) ![8]u8 {
    const value = std.hash.cityhash.CityHash64.hash(data);
    var buf: [8]u8 = undefined;
    _ = try std.fmt.bufPrint(&buf, "{x}", .{value});
    return buf;
}

Calling the function would cause an exception:

PHP Fatal error:  Uncaught ZigException: no space left in /home/cleong/zigar/php-zigar/demos/city-hash/src/hash.php:5
Stack trace:
#0 /home/rwiggum/city-hash/src/hash.php(5): hash->hash('Hello world')
#1 {main}
  thrown in /home/rwiggum/city-hash/src/hash.php on line 5

Instead of relying on knowing the exact length of the output, let us alter our function so that it returns a variable-length array. For that the function needs to receive an allocator from PHP:

const std = @import("std");
const CityHash64 = std.hash.cityhash.CityHash64;

pub fn hash(allocator: std.mem.Allocator, data: []const u8) ![]const u8 {
    const value = std.hash.cityhash.CityHash64.hash(data);
    return try std.fmt.allocPrint(allocator, "{x}", .{value});
}

No changes to the our code are required. The allocator argument is provided automatically by php-zigar when the function gets called.

Providing options

In this section, we'll enhance our function by allowing the user to supply a custom hash seed and to choose between upper-case and lower-case hex string. To archive this, we're going to add an options argument that accepts a struct:

const std = @import("std");
const CityHash64 = std.hash.cityhash.CityHash64;

const Options = struct {
    seed: ?u64 = null,
    seeds: ?[2]u64 = null,
    uppercase: bool = false,
};

pub fn hash(allocator: std.mem.Allocator, data: []const u8, options: Options) ![]const u8 {
    const value = if (options.seeds) |seeds|
        CityHash64.hashWithSeeds(data, seeds[0], seeds[1])
    else if (options.seed) |seed|
        CityHash64.hashWithSeed(data, seed)
    else
        CityHash64.hash(data);
    return if (options.uppercase)
        try std.fmt.allocPrint(allocator, "{X}", .{value})
    else
        try std.fmt.allocPrint(allocator, "{x}", .{value});
}

When the last argument of a function is a struct and all its fields have default values, it's treated as optional. Our PHP code still works even though the Zig function now takes three arguments.

To specify options, you can use an associative array:

<?php

$m = zigar_use(__DIR__ . '/../zig/hash.zig');

$hash = $m->hash("Hello world", [ 'uppercase' => true ]);
echo "$hash\n";
37C40368D1A84F24

Or an object:

<?php

$m = zigar_use(__DIR__ . '/../zig/hash.zig');

$options = new stdClass;
$options->uppercase = true;
$options->seed = 1234;
$hash = $m->hash("Hello world", $options);
echo "$hash\n";
13CF29EC3F75009D

Or named arguments:

<?php

$m = zigar_use(__DIR__ . '/../zig/hash.zig');

$hash = $m->hash("Hello world", uppercase: true, seeds: [ 1234, 5678 ]);
echo "$hash\n";
204936C7EF11DCAF

Making return value more PHP-friendly

In the example above, hash() returns a slice of u8. On the PHP side it's represented by an object. Our code works only because the echo operator automatically coerces values to strings. If we change our PHP code so that print_r() is used for output instead:

<?php

$m = zigar_use(__DIR__ . '/../zig/hash.zig');

$hash = $m->hash("Hello world", uppercase: true, seeds: [ 1234, 5678 ]);
print_r($hash);

We get the following:

[]const u8 Object
(
    [0] => 50
    [1] => 48
    [2] => 52
    [3] => 57
    [4] => 51
    [5] => 54
    [6] => 67
    [7] => 55
    [8] => 69
    [9] => 70
    [10] => 49
    [11] => 49
    [12] => 68
    [13] => 67
    [14] => 65
    [15] => 70
)

Zigar lets you flag certain functions as returning strings. To do so, you declare a struct type with a particular name at the root level:

const module_ns = @This();
pub const @"meta(zigar)" = struct {
    pub fn isDeclString(comptime T: type, comptime name: std.meta.DeclEnum(T)) bool {
        return switch (T) {
            module_ns => switch (name) {
                .hash => true,
                else => false,
            },
            else => false,
        };
    }
};

During export, isDeclString() is invoked when a function's return value is something that can be interpreted as a text string (e.g. []const u8). With the above declaration in place, the print_r() call now gets an actual string:

204936C7EF11DCAF

You can use the following to threat all occurences of u8 and u16 as text:

pub const @"meta(zigar)" = struct {
    pub fn isDeclString(comptime T: type, comptime _: std.meta.DeclEnum(T)) bool {
        return true;
    }

    pub fn isFieldString(comptime T: type, comptime _: std.meta.FieldEnum(T)) bool {
        return true;
    }
};

Configuring the app for deployment

We follow the same steps as described in the the hello world example. First we add build.php:

<?php

$targets = [
    [ 'platform' => 'linux', 'arch' => 'x64' ],
    [ 'platform' => 'linux', 'arch' => 'arm64' ],
    [ 'platform' => 'darwin', 'arch' => 'arm64' ],
    [ 'platform' => 'win32', 'arch' => 'x64' ],
];
foreach ($targets as $options) {
    zigar_compile(__DIR__ . '/../zig/hash.zig', $options);
}

Then we change the optimization setting in php.ini:

[zigar]
zigar.optimize=ReleaseSmall

Finally, we run the build script:

php src/build.php

Source code

You can find the complete source code for this example here.

Conclusion

Okay, that wasn't much of a server-side app. At least it does something. In the next example we'll build a server-side app for real, one that actually handles remote requests.


Image processing example