C strings are simple: they start at a memory address and end at the first zero/null byte (\0). But what if your data contains nulls? If you use standard string types like String or Array[Char], C (and Affix) will assume the data ends at the first null byte. This is fatal for encryption, compression, and binary formats.
To handle this, we must stop thinking in characters and start thinking in bytes.
The Recipe
This is a demonstration of the XOR cipher. Since the data might contain nulls, we handle it as a raw buffer.
use v5.40;
use Affix qw[:all];
use Affix::Build;
# Compile the C library
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
#include <stdlib.h>
#include <string.h>
char * string_crypt(const char * input, int len, const char * key) {
char * output = malloc(len + 1);
if (!output) return NULL;
output[len] = '\0';
int key_len = strlen(key);
for (int i = 0; i < len; i++) {
output[i] = input[i] ^ key[i % key_len];
}
return output;
}
void string_free(void * p) { free(p); }
END
my $lib = $c->link;
# Bind the functions
# Return Pointer[Void] to get the raw address (Pin) without interpretation.
affix $lib, 'string_crypt', [ String, Int, String ] => Pointer [Void];
affix $lib, 'string_free', [ Pointer [Void] ] => Void;
# The Wrapper
sub cipher( $input, $key ) {
my $len = length($input);
# Call C. We get back a Pin (unmanaged pointer).
my $ptr = string_crypt( $input, $len, $key );
return undef if is_null($ptr);
# Create a view of the memory as an Array of Bytes (UInt8).
# Important:
# Array[Char] => Reads up to NULL (String behavior)
# Array[UInt8] => Reads exactly $len bytes (Binary behavior)
my $binary_string = cast( $ptr, Array [ UInt8, $len ] );
# Clean up the C memory immediately
string_free($ptr);
return $binary_string;
}
# Run it
my $orig = 'hello world!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!';
my $key = 'foobar';
my $enc = cipher( $orig, $key );
my $dec = cipher( $enc, $key );
say "Original: $orig";
say "Decrypted: $dec";
# Verify the binary data size
say "Encrypted length: " . length($enc);
use JSON::PP;
say "Encrypted blob: " . encode_json( [$enc] );
How It Works
-
1. Pointer[Void]
We define the return type as
Pointer[Void]to prevent Affix from automatically attempting to read it as a null-terminated string. We receive a raw Pin. -
2. Casting to Array[UInt8]
my $view = cast( $ptr, Array[UInt8, $len] );We use
UInt8(unsigned 8-bit integer) instead ofChar. This tells Affix we are dealing with raw binary data, not text. Affix will read exactly$lenbytes from memory, regardless of nulls. Affix optimizes the operation and copies the raw bytes directly into a Perl scalar.
Kitchen Reminders
-
Char vs UInt8
In Affix,
Array[Char]is treated as a string buffer (stops atNULL).Array[UInt8]is treated as a byte buffer (reads full length). Use the one matching your data.