RZig lets an R package implement functions in Zig. The package author writes ordinary Zig functions and marks the exported ones; RZig generates the registration, the R wrappers and the conversion of every argument and result, and runs the Zig build when the package is installed. This vignette goes through the workflow one step at a time. Every step up to the installation is plain R and runs without Zig; the steps that compile run when a supported Zig compiler is found, and this copy of the vignette ran them.
Find the compiler
find_zig() looks for a Zig release of the supported
series (0.16 and 0.17) in ZIG, on PATH, in the
folder of install_zig() and in the usual install locations.
With required = FALSE, a missing compiler is a result and
not an error:
compiler
#> Zig compiler: /home/runner/work/_temp/0beafb3f-08c9-475d-9a85-dbeb3ad24ddb/zig-x86_64-linux-0.16.0/zig (version 0.16.0, supported)
#> rzig supports Zig 0.16.x or 0.17.x.A user without Zig can install a verified release with
install_zig(), which downloads it into the user cache
folder of rzig after asking for permission.
Create a package
create_rzig_package() creates a new package with the
RZig scaffold, a starter function and a test; use_rzig()
adds the same scaffold to an existing package. The package here lives in
R’s temporary directory:
path <- file.path(tempdir(), "hellozig")
create_rzig_package(path, fields = list(
Title = "Greetings from Zig",
Description = "Shows the workflow of 'rzig'."
))
#> Created package hellozig at /tmp/RtmpEtatYD/hellozig
#> Write Zig functions in src/rzig/src/main.zig, then run rzig::document(path) and rzig::build_zig(path)The Zig code lives in src/rzig/src/main.zig. The starter
function shows the three parts of an export: documentation comments,
which become the help page, the marker /// @export, and a
public function with supported types:
main_zig <- file.path(path, "src", "rzig", "src", "main.zig")
cat(readLines(main_zig), sep = "\n")
const std = @import("std");
const builtin = @import("builtin");
const rzig = @import("rzig");
/// Route ReleaseSafe failures through R while retaining Zig test behavior.
pub const panic = if (builtin.is_test)
std.debug.FullPanic(std.debug.defaultPanic)
else
rzig.Panic;
/// Return a friendly greeting.
/// @param name A name to greet.
/// @return A character string.
/// @examples
/// hello_zig("R")
/// @export
pub fn hello_zig(ctx: *rzig.Ctx, name: []const u8) rzig.Error![]const u8 {
return std.fmt.allocPrint(ctx.allocator(), "Hello, {s}!", .{name}) catch
rzig.raise("out of memory while building the greeting", .{});
}
comptime {
rzig.registerModule(@This());
}Add a function
A second export takes a numeric vector, which Zig reads as a borrowed
slice, and returns a numeric vector that Zig allocates in the call
context ctx:
cat(
"",
"/// The cumulative sum of a numeric vector.",
"/// @param x A numeric vector.",
"/// @return A numeric vector of the same length.",
"/// @examples",
"/// cumulative(c(1, 2, 3))",
"/// @export",
"pub fn cumulative(ctx: *rzig.Ctx, x: []const f64) rzig.Error![]f64 {",
" const out = try ctx.alloc(f64, x.len);",
" var total: f64 = 0;",
" for (x, out) |value, *slot| {",
" total += value;",
" slot.* = total;",
" }",
" return out;",
"}",
sep = "\n", file = main_zig, append = TRUE
)Scan, generate and check
scan_exports() lists the exported functions with their R
arguments and the R values they accept and return:
scan_exports(path)
#> Zig exports of package hellozig (src/rzig/src/main.zig): 2 functions
#>
#> hello_zig(name)
#> name []const u8 character, length 1, copied
#> returns rzig.Error![]const u8 character, length 1
#>
#> cumulative(x)
#> x []const f64 double vector, borrowed
#> returns rzig.Error![]f64 double vectordocument() writes the generated files: the Zig manifest,
the R wrappers, the NAMESPACE lines and the help pages. Run
it after every change of an exported signature or its documentation:
bindings <- document(path)
#> Generated RZig bindings for hellozig: 2 exported functions
cat(bindings$wrappers)
#> # Generated by rzig::document(). Do not edit.
#>
#> #' Return a friendly greeting.
#> #' @param name A name to greet.
#> #' @return A character string.
#> #' @examples
#> #' hello_zig("R")
#> #'
#> #' @export
#> hello_zig <- function(name) {
#> .Call(hello_zig_, name)
#> }
#>
#> #' The cumulative sum of a numeric vector.
#> #' @param x A numeric vector.
#> #' @return A numeric vector of the same length.
#> #' @examples
#> #' cumulative(c(1, 2, 3))
#> #'
#> #' @export
#> cumulative <- function(x) {
#> .Call(cumulative_, x)
#> }rzig_status() runs the checks that
build_zig() runs before it compiles: the
DESCRIPTION, the scaffold, the Zig source, the types of the
exports, the generated files and the toolchain:
rzig_status(path)
#> RZig status of package hellozig
#> [ok] DESCRIPTION package hellozig
#> [ok] Scaffold build files and framework sources present
#> [ok] Zig source src/rzig/src/main.zig
#> [ok] Exports 2 functions: hello_zig, cumulative
#> [ok] Bindings generated files match the exports
#> [ok] Zig compiler /home/runner/work/_temp/0beafb3f-08c9-475d-9a85-dbeb3ad24ddb/zig-x86_64-linux-0.16.0/zig
#> (version 0.16.0)
#> [ok] C toolchain C compiler gccBuild and call
build_zig() installs the package from a copy, so that
the source folder stays clean, and reports compiler errors by file and
line:
build <- build_zig(path)
build
#> Build of package hellozig: succeeded in 9.4 seconds
#> Zig compiler: /home/runner/work/_temp/0beafb3f-08c9-475d-9a85-dbeb3ad24ddb/zig-x86_64-linux-0.16.0/zig (version 0.16.0)
#> Installed into a library in the session temporary directory
library(build$package, character.only = TRUE, lib.loc = build$library)
hello_zig("R")
#> [1] "Hello, R!"
cumulative(c(1, 2, 3, 4))
#> [1] 1 3 6 10Arguments of the wrong type are R errors that name the argument; the R session continues:
try(cumulative("a"))
#> Error : rzig: argument `x` of cumulative must be a numeric vector; got character; use as.numeric() in RTest the Zig code
Functions that do not use the rzig module can be tested
with Zig’s own test runner, without R. test_zig() runs the
test blocks reached from
src/rzig/src/tests.zig:
writeLines(c(
"const std = @import(\"std\");",
"",
"fn add(a: f64, b: f64) f64 {",
" return a + b;",
"}",
"",
"test \"add\" {",
" try std.testing.expectEqual(@as(f64, 3), add(1, 2));",
"}"
), file.path(path, "src", "rzig", "src", "tests.zig"))
tests <- test_zig(path)
tests
#> Zig tests passed in 5.0 secondsTry Zig from the R prompt
zig_function() compiles one Zig function into a
temporary package and returns an R function, which suits experiments
before code moves into a package:
twice <- zig_function("pub fn twice(x: f64) f64 { return 2 * x; }")
twice(21)
#> [1] 42