RZig lets R package authors write plain Zig functions and generate
safe .Call bindings without C++ or hand-written
SEXP conversion code.
0.16.x or
0.17.x), needed only to install a package created with
RZig. RZig itself, its examples and its tests run without Zig. Zig
changes its language and standard library between release series, so
other versions are rejected with a message naming the version
found.Code that has to build with both series is best formatted with the
zig fmt of Zig 0.16: the formatter of Zig 0.17 rewrites
@intFromEnum and @enumFromInt into the
builtins @backingInt and @fromBackingInt,
which Zig 0.16 does not have.
configure compiles the Zig code for the platform of R:
x86_64 or arm64 macOS for the macOS version that R links for
(MACOSX_DEPLOYMENT_TARGET, or that of
libR.dylib), x86_64 or arm64 Linux with glibc or musl, and
x86_64 or arm64 Windows. RZIG_ZIG_TARGET names another Zig
target, for example RZIG_ZIG_TARGET=x86_64-linux-musl.
Download Zig from https://ziglang.org/download/ and confirm the version:
zig version
# 0.16.0From CRAN:
install.packages("rzig")The development version from GitHub:
install.packages("remotes")
remotes::install_github("MarekDejaUJ/rzig")If Zig is not on PATH, tell RZig where it is:
Sys.setenv(ZIG = "/absolute/path/to/zig")find_zig() and the generated package configuration
search in this order: ZIG, PATH,
~/.local/share/zig/*/zig, then ~/zig/zig. An R
session launched from an IDE may not inherit variables exported by a
shell; in that case, set ZIG with Sys.setenv()
first.
rzig::find_zig(required = FALSE)
#> Zig compiler: /usr/local/bin/zig (version 0.16.0, supported)
#> rzig supports Zig 0.16.x or 0.17.x.The workflow has five steps, each an R function. For a
copy-paste-safe example, create the package in R’s session temporary
directory. Replace tempdir() with an explicit project
directory when keeping the result.
1. Scaffold. use_rzig() adds the build
files, the framework sources and a starter
src/rzig/src/main.zig with a working
hello_zig() function:
pkg <- file.path(tempdir(), "rzhello")
dir.create(pkg)
writeLines(
c(
"Package: rzhello",
"Type: Package",
"Title: A Small RZig Example",
"Version: 0.0.1",
"Authors@R: person('Your', 'Name', email = 'you@example.org', role = c('aut', 'cre'))",
"Description: Demonstrates a Zig implementation called safely from R.",
"License: GPL-3",
"Encoding: UTF-8"
),
file.path(pkg, "DESCRIPTION")
)
rzig::use_rzig(pkg)
#> Generated RZig bindings for rzhello: 1 exported function
#> Created RZig scaffold for package rzhello2. Write Zig. Open
src/rzig/src/main.zig, keep its imports, panic
declaration and comptime registration block, and replace
the generated hello_zig() function with:
/// Add two numeric vectors elementwise.
/// @param a The first numeric vector.
/// @param b The second numeric vector.
/// @return The elementwise sums.
/// @export
pub fn add_vectors(
ctx: *rzig.Ctx,
a: []const f64,
b: []const f64,
) rzig.Error![]f64 {
if (a.len != b.len) {
return rzig.raise("lengths differ: {d} vs {d}", .{ a.len, b.len });
}
const result = try ctx.alloc(f64, a.len);
for (a, b, result) |left, right, *value| value.* = left + right;
return result;
}The @param and @return lines are optional.
When they are absent, document() generates neutral
placeholders from the Zig signature. Only /// @export is
required to expose a public function to R. Exported functions are
top-level pub fn declarations starting at the beginning of
a line, as zig fmt writes them.
3. Scan. scan_exports() lists what R
will see, with the R value of every parameter and of the return
value:
rzig::scan_exports(pkg)
#> Zig exports of package rzhello (src/rzig/src/main.zig): 1 function
#>
#> add_vectors(a, b)
#> a []const f64 double vector, borrowed
#> b []const f64 double vector, borrowed
#> returns rzig.Error![]f64 double vector4. Generate. document() writes the Zig
manifest, the R wrappers and the NAMESPACE block;
render_bindings() shows the same texts without writing
them, and rzig_status() reports whether every step is
complete:
rzig::document(pkg)
#> Generated RZig bindings for rzhello: 1 exported function
rzig::rzig_status(pkg)
#> RZig status of package rzhello
#> [ok] DESCRIPTION package rzhello
#> [ok] Scaffold build files and framework sources present
#> [ok] Zig source src/rzig/src/main.zig
#> [ok] Exports 1 function: add_vectors
#> [ok] Bindings generated files match the exports
#> [ok] Zig compiler /usr/local/bin/zig (version 0.16.0)
#> [ok] C toolchain C compiler clangrzig_status() also reports, before anything is compiled,
argument and return types the boundary rejects, more than 32 arguments,
misuse of rzig.Mut([]f64), a missing registration block or
panic handler in main.zig, R functions of the same name
elsewhere in R/, framework sources from another rzig
version, non-executable configure scripts and a missing C
toolchain. A failed build names its cause:
rzig::build_zig(pkg, preflight = FALSE)
#> Build of package rzhello: failed at the compile step
#> src/rzig/framework/convert.zig:138:9 (function total)
#> rzig: function `total`, parameter 1 has unsupported type '[]const f32'.
#> nearest supported alternative: []const f64 for an R numeric vector
#> The complete installation log (61 lines) is in $log.5. Build and call. build_zig() runs the
checks of rzig_status(), compiles and installs the package
from a temporary copy, and reports the first compiler, linker or load
error with its file and line when the build fails. The package also
installs like any source package, with
install.packages(pkg, repos = NULL, type = "source") or
R CMD INSTALL.
build <- rzig::build_zig(pkg)
build
#> Build of package rzhello: succeeded in 8.1 seconds
#> Zig compiler: /usr/local/bin/zig (version 0.16.0)
#> Installed into a library in the session temporary directory
library(rzhello, lib.loc = build$library)
add_vectors(c(1, 2, 3), c(10, 20, 30))
#> [1] 11 22 33
add_vectors(c(1, 2), c(1, 2, 3))
#> Error: lengths differ: 2 vs 3
# The session remains usable after the native error.
add_vectors(c(4, 5), c(6, 7))
#> [1] 10 12The same workflow applies to an existing package: run
use_rzig() once, edit src/rzig/src/main.zig,
and run document() whenever exported Zig signatures or
comments change. The scaffolded cleanup and
cleanup.win scripts remove generated Makevars and Zig build
caches after package installation and checks. use_rzig()
stops when src/ already holds C, C++ or Fortran sources
(see the next section for such packages) or a hand-written
src/Makevars, whose settings belong in
src/Makevars.in and src/Makevars.win.in.
use_rzig(path, mixed = TRUE) (or
use_rzig_mixed(path) in a package that already uses rzig)
sets Config/rzig/mixed: true. R then compiles every C, C++
and Fortran source of src/ with its own compilers and links
them with the Zig library, and src/entry.c leaves
R_init_<pkg> to the package. The package’s
R_init_<pkg> registers its routines with
rzig_register(), which adds the Zig routines to the
.Call routines:
#include <R_ext/Rdynload.h>
void rzig_register(DllInfo *dll, const R_CMethodDef *c_methods,
const R_CallMethodDef *call_methods,
const R_FortranMethodDef *fortran_methods,
const R_ExternalMethodDef *external_methods);
static const R_CallMethodDef call_methods[] = {
{"legacy_add", (DL_FUNC) &legacy_add, 2},
{NULL, NULL, 0}
};
void R_init_mypkg(DllInfo *dll) {
rzig_register(dll, NULL, call_methods, NULL, NULL);
}The R code keeps its own useDynLib() directive for the C
routines and may name them by symbol or by string.
rzig_status() and build_zig() report a mixed
package whose sources do not call rzig_register().
A package can ship the sources of a C library, for example under
src/rzig/vendor/answer, and list it in
src/rzig/c_libraries.zon. Zig compiles the sources into the
package and translates the header into a Zig module; nothing is
downloaded.
.{
.{
.name = "answer",
.header = "vendor/answer/answer.h",
.sources = .{"vendor/answer/answer.c"},
.include = .{"vendor/answer"},
.flags = .{"-std=c99"},
},
}The Zig sources then call the library through
const answer = @import("answer");, also in the tests of
test_zig(). Paths are relative to src/rzig;
rzig_status() reports listed files that do not exist, and
update_rzig() never replaces the file. The license and
copyright of the vendored code belong in the package’s
DESCRIPTION and inst/COPYRIGHTS.
A package shares Zig code as a module:
use_rzig_module(path, "kernels") creates
inst/zig/kernels/root.zig, which R installs with the
package. Another package uses it with
use_rzig_import(path, "kernelpkg", "kernels"), which adds
kernelpkg/kernels to Config/rzig/imports and
kernelpkg to LinkingTo, so that R installs
kernelpkg first. Its Zig code then imports the module:
const kernels = @import("kernels");
/// Twice the sum of the values.
/// @export
pub fn total(x: []const f64) f64 {
return 2 * kernels.sum(x);
}configure finds the module in the installed package, and
the module is compiled into the importing package, so nothing is linked
between packages at run time. A module can import rzig and
the other imported modules; test_zig() passes the imports
to the Zig tests as well.
Compiled code of other packages, in C or in Zig, can call functions
that a package registers with R_RegisterCCallable(). The
root Zig file lists them, each with callconv(.c), and may
declare an interface version:
fn scaledSum(x: [*]const f64, n: usize, factor: f64) callconv(.c) f64 {
var total: f64 = 0;
for (x[0..n]) |value| total += value;
return factor * total;
}
pub const rzig_callables = .{ .scaled_sum = scaledSum };
pub const rzig_interface_version: u32 = 2;Another rzig package fetches one with rzig.callable(),
which loads the namespace of the providing package and checks the
interface version, because R checks neither the signature nor a
version:
const ScaledSum = fn ([*]const f64, usize, f64) callconv(.c) f64;
const scaled_sum = try rzig.callable(ctx, ScaledSum, "provpkg", "scaled_sum", .{ .interface = 2 });C code calls R_GetCCallable("provpkg", "scaled_sum")
after loading the namespace; the providing package can declare the C
signatures in inst/include. The calling package lists the
providing one in Imports.
zig_function() compiles Zig code into a small package in
the session’s temporary directory and defines its functions, like
Rcpp::cppFunction():
twice <- rzig::zig_function("pub fn twice(x: f64) f64 { return 2 * x; }")
twice(21)
#> [1] 42
rzig::zig_eval("@sqrt(2.0)", type = "f64")
#> [1] 1.414214zig_source(file) does the same for a file. Every
top-level pub fn is exported unless the code marks
functions with /// @export. Builds are cached for the
session, and a compiler error is reported with the line of the code. In
R Markdown and Quarto documents, a zig chunk compiles its
code with zig_function() and later R chunks can call the
functions.
ctx.print() and ctx.eprint() format text
with Zig’s std.fmt and write it to R’s output and message
streams (Rprintf, REprintf), so it reaches the
console, RStudio and sink().
ctx.progress(total, show) draws a progress bar on the
message stream and checks for a user interrupt at each redraw:
/// @default verbose interactive()
/// @export
pub fn simulate(ctx: *rzig.Ctx, n: usize, verbose: bool) rzig.Error!f64 {
var progress = ctx.progress(n, verbose);
var total: f64 = 0;
for (0..n) |i| {
total += @sqrt(@as(f64, @floatFromInt(i)));
try progress.add(1);
}
try progress.finish();
return total;
}All three need the call context, so they run on the R thread; a
progress bar in the state of rzig.parallelFor workers is a
compile error.
Packages build in Zig’s ReleaseSafe mode: optimized,
with the bounds, overflow and unreachable checks that the
boundary turns into R errors.
build_zig(path, optimize = "Debug"), or
RZIG_OPTIMIZE=Debug in the environment of
R CMD INSTALL, builds without optimization and with the
same checks and complete stack traces. ReleaseFast and
ReleaseSmall are rejected, because they remove the checks;
@setRuntimeSafety(false) removes them from a single
function whose safety the package author has established.
test_zig(path) runs zig build test for a
package: the test blocks of the files that
src/rzig/src/tests.zig imports are compiled for the host
and run without R, which suits numerical kernels written in plain
Zig:
// src/rzig/src/tests.zig
test {
_ = @import("kernels.zig");
}Files that use the rzig module call into R and are
tested from R (testthat). Packages scaffolded by an older
rzig get the test step of build.zig from
update_rzig(path).
install_zig() downloads Zig 0.16.0 (or
install_zig("0.17.0")) for the current platform into
zig_home(), a folder in rzig’s user cache. The archive is
checked against the size and SHA-256 that ziglang.org publishes, pinned
in rzig, and comes from a community mirror of Zig or from ziglang.org.
find_zig() and the configure scripts of rzig
packages find compilers installed there; uninstall_zig()
removes them.
The configure script of a package created with rzig
downloads Zig itself when no supported compiler is found and the
environment variable RZIG_DOWNLOAD_ZIG is
true:
Sys.setenv(RZIG_DOWNLOAD_ZIG = "true")
install.packages("zigstats", repos = "https://example.r-universe.dev")The download is the verified archive of install_zig()
(Zig 0.16.0, or the release named by RZIG_ZIG_VERSION) and
is kept in zig_home() for later installations. Without the
variable, nothing is downloaded and the installation stops with
instructions.
create_rzig_package("path/to/zigstats") creates a
package named zigstats with the scaffold, the
hello_zig() example, its help page and a testthat suite;
build_zig("path/to/zigstats") installs it. Replace the
placeholder title, description and author through
fields:
rzig::create_rzig_package("zigstats", fields = list(
Title = "Rank Statistics in 'Zig'",
"Authors@R" = 'person("Ada", "Lovelace", email = "ada@example.org", role = c("aut", "cre"))'
))use_rzig_ci(path) writes
.github/workflows/rzig-check.yaml, a GitHub Actions
workflow that installs R and Zig 0.16.0 and 0.17.0 and runs
R CMD check --as-cran on Linux, macOS and Windows, after
the Zig tests when src/rzig/src/tests.zig exists. The
zig and os arguments choose the matrix.
The marker line and @default lines shape the R
wrapper:
/// Fit the model.
/// @param x The data.
/// @param tolerance The convergence tolerance.
/// @default tolerance 1e-8
/// @export name=fit_model invisible
pub fn fit_model_zig(x: []const f64, tolerance: f64) f64 { ... }generates
fit_model <- function(x, tolerance = 1e-8) invisible(...).
The options are internal (see below),
invisible and name=<R name>; any other
option is an error of the scan.
main.zig can collect exports from other files of
src/rzig/src:
pub const stats = @import("stats.zig"); // exports in stats.zig are found
const helpers = @import("helpers.zig");
/// Three times a value.
/// @export
pub const triple = helpers.triple; // re-exports helpers.triple as triple()A function marked /// @export in a file that
main.zig imports with pub const (directly or
through another such file) is registered under its own name. A marker in
a file imported with a plain const is an error, because the
generated registration can reach only public declarations; a marked file
that is not imported at all is ignored, and rzig_status()
notes it. R names must be unique across all files.
A native routine that only the package’s own R code calls is marked
/// @export internal. It is registered and gets an R
wrapper, but the wrapper is not exported from the package and gets no
help page:
/// Fit the penalized model for the R function fit_model().
/// @export internal
pub fn fit_model_zig(ctx: *rzig.Ctx, x: rzig.Matrix, y: []const f64) rzig.Error![]f64 {
...
}fit_model <- function(x, y) {
stopifnot(is.matrix(x), is.numeric(y))
fit_model_zig(x, as.double(y))
}Every installation compiles in a fresh copy of the package, and Zig
keeps its global cache inside that copy so that R CMD check
and CRAN find nothing written outside the package. With Zig 0.17 the
first step of each build then compiles Zig’s build runner again. On a
development machine, a persistent cache avoids that:
Sys.setenv(RZIG_ZIG_CACHE = tools::R_user_dir("rzig", "cache"))Leave RZIG_ZIG_CACHE unset for checks of a package
before release.
After installing a new version of rzig, bring a package up to date with
rzig::update_rzig(pkg)update_rzig() replaces the framework sources under
src/rzig/framework, updates every build file that is still
an unmodified rzig template, adds missing ones and regenerates the
bindings. It never changes src/rzig/src or files rzig does
not know, and it keeps build files the package author edited (for
example a configure that downloads Zig, or a
build.zig with an extra test step) and lists them, so their
differences from the new templates can be reviewed.
update_rzig(pkg, force = TRUE) replaces those as well.
rzig_status() reports framework sources that do not match
the installed rzig.
If roxygen2 generates NAMESPACE (its first line is
# Generated by roxygen2: do not edit by hand),
document() writes the native-symbol directive into
R/rzig-wrappers.R as a @rawNamespace tag and
adds the same lines roxygen2 writes to NAMESPACE. The
package then works whichever of rzig::document(),
roxygen2::roxygenise() or devtools::document()
ran last, and roxygen2 writes the help pages of the exported Zig
functions. Run rzig::document() after changing an exported
Zig signature, so that the wrappers follow it. When devtools and rzig
are both attached, one document() masks the other; call
rzig::document() explicitly. If roxygenise()
loaded the package in the current R session, restart R before calling
newly installed native exports.
document()
generatesEvery public Zig function with a /// @export line
produces:
NAMESPACE;man/<name>.Rd written from the same
comment (the first paragraph gives the title and description;
@param, @return, @examples,
@details, @seealso, @references
and @note fill their sections). In a package documented
with roxygen2, roxygen2 writes the page from the roxygen block
instead.The first *rzig.Ctx parameter is supplied by RZig and
omitted from the R function. The complete input surface is:
| Zig parameter | Accepted R value | Conversion |
|---|---|---|
f64 |
length-one double, integer, or logical | checked scalar |
i32 |
length-one double, integer, or logical | checked whole number |
bool |
length-one logical | checked scalar |
usize |
length-one double, integer, or logical | checked whole number from 0 to 2147483647 |
i64 |
length-one bit64::integer64, double, or integer |
checked whole number; doubles up to 2^53 - 1 in absolute value |
?f64, ?i32, ?bool,
?usize, ?i64 |
corresponding scalar, NA, or NULL |
missing values become null |
[]const f64 |
double vector | borrowed, read-only |
[]const i32 |
integer vector | borrowed, read-only |
[]const i64 |
bit64::integer64 vector |
borrowed, read-only; NA is
rzig.NA.integer64 |
[]const bool |
logical vector without NA |
copied into the call arena |
[]const rzig.Logical |
logical vector | borrowed, read-only; elements .TRUE,
.FALSE, .NA |
[]const u8 |
one non-NA character value |
copied as UTF-8 |
[]const []const u8 |
character vector without NA |
copied as UTF-8 |
[]const ?[]const u8 |
character vector | copied as UTF-8; NA becomes null |
?[]const u8 |
one character value, NA, or NULL |
copied as UTF-8; NA and NULL become
null |
rzig.Complex |
length-one complex, double, or integer | checked scalar; NA errors |
[]const rzig.Complex |
complex vector | borrowed, read-only, layout of Rcomplex |
rzig.Raw |
raw vector | borrowed bytes in .data |
rzig.Record(T) |
named list | entries read into the fields of struct T by name;
defaults fill missing entries |
rzig.DataFrame(T) |
data frame | columns read into the fields of struct T;
.nrow rows |
rzig.Factor |
factor | .codes borrowed, .levels copied |
rzig.CscMatrix |
Matrix::dgCMatrix |
slots i, p, x borrowed and
checked; column(j) |
?T |
T, or NULL |
NULL becomes null (scalars also map
NA to null) |
rzig.Matrix |
double matrix | borrowed, read-only, column-major |
rzig.IntMatrix |
integer matrix | borrowed, read-only, column-major; NA is
std.math.minInt(i32) |
rzig.BoolMatrix |
logical matrix without NA |
copied into the call arena |
rzig.Mut([]f64), rzig.Mut([]i32) |
double or integer vector | duplicated before writable access |
rzig.Mut(rzig.Matrix),
rzig.Mut(rzig.IntMatrix) |
double or integer matrix | duplicated with its attributes before writable access |
rzig.Sexp |
any R object | borrowed low-level handle |
rzig.Function |
function | borrowed; call() evaluates it from Zig |
Supported return values are:
| Zig return | R result |
|---|---|
void |
NULL |
f64, i32, bool |
length-one double, integer, or logical |
i64, []const i64 / []i64 |
bit64::integer64 value or vector |
[]const f64 / []f64 |
double vector |
[]const i32 / []i32 |
integer vector |
[]const bool / []bool |
logical vector |
[]const rzig.Logical / []rzig.Logical |
logical vector that can contain NA |
[]const u8 |
length-one character vector |
[]const []const u8 / [][]const u8 |
character vector |
[]const ?[]const u8 / []?[]const u8 |
character vector; null becomes NA |
rzig.Complex |
length-one complex vector |
[]const rzig.Complex / []rzig.Complex |
complex vector |
rzig.Raw |
raw vector |
rzig.Record(T) / rzig.DataFrame(T) |
named list / data frame built from the fields of T |
rzig.Factor |
factor |
rzig.List |
named R list |
rzig.Attributed(T) |
supported vector T with names, dimensions, dimnames, or
classes |
rzig.Sexp |
the supplied R object |
rzig.Function |
the supplied function |
?T |
supported T, or NULL when
null |
Any supported return may be wrapped in rzig.Error!T.
usize and the matrix views are parameter-only. Unsupported
signatures fail at compile time with the function and parameter position
in the error.
rzig.raise() records a message and returns
rzig.Error, so it can be returned directly or propagated
with try. rzig.warn() queues a warning for
delivery after Zig cleanup and returns void, so call it
without try:
if (values.len == 0) rzig.warn("received an empty vector", .{});rzig.List collects supported return values in Zig-owned
memory and converts them to one named R list only after the function
returns:
/// Return values together with their length.
/// @export
pub fn summarize(ctx: *rzig.Ctx, values: []const f64) rzig.Error!rzig.List {
if (values.len > std.math.maxInt(i32)) return rzig.raise("too many values", .{});
var result = rzig.List.init(ctx);
try result.put("values", values);
try result.put("count", @as(i32, @intCast(values.len)));
return result;
}List entries may contain f64, i32,
bool, double, integer, or logical slices, UTF-8 strings or
string vectors, optional values, or rzig.Sexp. Names are
copied into the call context, and no R object is allocated until
boundary conversion begins.
rzig.Record(T) reads a named list into the struct
T, field by field; a field with a default value may be
missing from the list, an optional field becomes null, and
an unknown entry gives a warning. One record argument carries any number
of values, beyond the 32 arguments of a function:
const Options = struct {
tolerance: f64 = 1e-8,
@"max.iter": i32 = 100,
weights: ?[]const f64 = null,
};
/// @export
pub fn fit(x: []const f64, options: rzig.Record(Options)) f64 {
_ = options.value.@"max.iter";
return x[0] * options.value.tolerance;
}rzig.DataFrame(T) reads the columns of a data frame
(slices or rzig.Factor) and its number of rows; other
columns are ignored, and every column must have one value per row.
rzig.Factor holds the 1-based codes and the levels of a
factor. All three are also return types: a record becomes a named list,
a data frame gets compact row names, and a factor’s codes are checked
against its levels.
A struct marked /// @class becomes an R class:
init is the constructor, exported under the struct’s name,
and every public function that takes the struct as its first parameter
is a method, called with $:
/// A running mean.
/// @class
pub const RunningMean = struct {
total: f64 = 0,
count: usize = 0,
/// Create an empty running mean.
pub fn init() RunningMean {
return .{};
}
/// Add values.
pub fn add(self: *RunningMean, values: []const f64) void {
for (values) |value| self.total += value;
self.count += values.len;
}
/// The mean of the values added so far.
pub fn mean(self: *const RunningMean) f64 {
return self.total / @as(f64, @floatFromInt(self.count));
}
};m <- RunningMean()
m$add(c(1, 2, 3))
m$mean()
#> [1] 2document() generates the constructor, a $
method, a print method that lists the methods, completion
of method names, and a help page with a Methods section; the object is
an rzig.External(RunningMean).
rzig.External(T) keeps a Zig value between calls in an R
object (an external pointer with the classes
c("T", "rzig_external")). A function returns
rzig.External(T).create(ctx, value), and later calls
receive the value through .ptr and may modify it; when R
garbage-collects the object, T.deinit() runs (it must not
call R) and the memory is released:
const Counter = struct {
count: i32 = 0,
pub fn deinit(self: *Counter) void {
_ = self;
}
};
/// @export
pub fn counter(ctx: *rzig.Ctx) rzig.Error!rzig.External(Counter) {
return rzig.External(Counter).create(ctx, .{});
}
/// @export
pub fn increment(c: rzig.External(Counter)) i32 {
c.ptr.count += 1;
return c.ptr.count;
}Memory that the value refers to must outlive the call: allocate it
with std.heap.c_allocator and free it in
deinit, not with the call context. An object of another
type or package is rejected by its tag. A value saved with
saveRDS() or in a workspace is lost, and using it is an
error that asks to create it again.
An rzig.Sexp parameter borrows any R object. Its
read-only methods report kind() (.double,
.list, .s4, …), length(),
isNull(), isObject(), isS4() and
inherits(class), read attribute(name) (S4
slots are attributes), names(ctx) and
classes(ctx), walk a list with element(index)
and field(name), and convert a value with
get(T, ctx) by the rules of a parameter of type
T:
/// @export
pub fn total(ctx: *rzig.Ctx, value: rzig.Sexp) rzig.Error!f64 {
const entry = (try value.field("x")) orelse return rzig.raise("no entry x", .{});
var sum: f64 = 0;
for (try entry.get([]const f64, ctx)) |x| sum += x;
return sum;
}Mutation is opt-in with rzig.Mut([]f64),
rzig.Mut([]i32), rzig.Mut(rzig.Matrix) or
rzig.Mut(rzig.IntMatrix). RZig duplicates and protects the
R vector or matrix before Zig receives writable storage, then returns
that duplicate, which keeps the attributes of the input
(dim, dimnames, class):
/// Scale a copy of a numeric vector.
/// @export
pub fn scale(values: rzig.Mut([]f64), factor: f64) void {
for (values.data) |*value| value.* *= factor;
}The caller’s vector and any aliases remain unchanged. A function
using Mut accepts one mutable vector and returns
void or rzig.Error!void; its generated R
wrapper returns the mutated duplicate automatically.
rzig.Matrix borrows an R double matrix in column-major
order and provides its validated shape through nrow and
ncol; rzig.IntMatrix borrows an integer
matrix, and rzig.BoolMatrix copies a logical matrix without
NA. at(row, column) reads one element and
column(j) returns one column as a slice (zero-based
indices). A matrix of another storage mode is rejected with the
storage.mode() call that converts it in R.
rzig.Mut(rzig.Matrix) exposes
set(row, column, value) as well:
/// Center each column of a numeric matrix.
/// @export
pub fn center_columns(values: rzig.Mut(rzig.Matrix)) void {
for (0..values.data.ncol) |j| {
const column = values.data.column(j);
var mean: f64 = 0;
for (column) |value| mean += value;
mean /= @floatFromInt(column.len);
for (column) |*value| value.* -= mean;
}
}Use rzig.Attributed([]const f64) to return a numeric
vector with metadata:
pub fn labeled(
ctx: *rzig.Ctx,
values: []const f64,
labels: []const []const u8,
) rzig.Error!rzig.Attributed([]const f64) {
var result = rzig.Attributed([]const f64).init(ctx, values);
try result.setNames(labels);
try result.setClass("labeled_values");
return result;
}setNames, setDim, setClass,
and setClasses copy their metadata into the call context;
setMatrix(nrow, ncol) sets a two-dimensional
dim and setDimnames(row_names, column_names)
names the rows and columns (null leaves a dimension
unnamed). Lengths and dimension products are checked before R
allocation.
Call try rzig.checkInterrupt() about every 100,000
iterations of a long loop. It probes through an R trampoline that
catches the runtime’s non-local interrupt exit, allowing Zig cleanup to
finish before the boundary returns an R error.
rzig.Rmath.density, rzig.Rmath.cdf and
rzig.Rmath.quantile evaluate the distributions of R
(.normal, .uniform, .gamma,
.beta, .binomial, .poisson,
.exponential, .t, .chisq,
.f, .lognormal, .weibull,
.logistic, .cauchy, .geometric,
.hypergeometric, .negative_binomial), with R’s
parameter names and defaults, and
rng.draw(distribution, params) draws from R’s generator, so
the draws follow set.seed():
const p = try rzig.Rmath.cdf(.gamma, x, .{ .shape = 2 }, .{ .lower_tail = false });
const d = try rzig.Rmath.density(.normal, x, .{ .mean = 1 }, .{ .log = true });
const rng = try ctx.rng();
const draw = rng.draw(.poisson, .{ .lambda = 3 });rzig.parallelReduce(ctx, T, count, &state, identity, map, combine)
reduces map(state, i) with combine in chunks
fixed by count, so a parallel floating-point sum is the
same for every number of threads. ctx.rngStreams() gives
workers random streams seeded from R’s stream:
streams.stream(i) is the same for the same seed and index,
so parallel draws follow set.seed(). A user interrupt stops
the workers after their current index, and a panic on a worker (for
example a failed bounds check) becomes an R error instead of ending the
session.
rzig.Blas (gemm, gemv,
syrk, trsm) and rzig.Lapack
(potrf, potrs, getrf,
getrs, gesv, geqrf,
ormqr, gels, syevr,
gesdd) call the BLAS and LAPACK that R uses, on
column-major matrices: rzig.Matrix operands and
rzig.MutableMatrix results, for example from
ctx.matrix(nrow, ncol) or
rzig.Mut(rzig.Matrix). Every routine checks the shapes
before the call and turns LAPACK’s info into an R error,
such as “the matrix is not positive definite”. A package that uses them
runs use_rzig_lapack(path) once, so that it links
$(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS):
/// @export
pub fn crossprod(ctx: *rzig.Ctx, x: rzig.Matrix) rzig.Error!rzig.Attributed([]f64) {
const out = try ctx.matrix(x.ncol, x.ncol);
try rzig.Blas.gemm(ctx, .yes, .no, 1, x, x, 0, out);
var result = rzig.Attributed([]f64).init(ctx, out.data);
try result.setMatrix(out.nrow, out.ncol);
return result;
}An rzig.Function parameter receives an R function, and
f.call(ctx, T, args) calls it on the thread that entered R.
args is a tuple of positional arguments or a struct of
named ones, with values of any type an exported function can return; the
result is converted to T like an argument
(void ignores it).
pub fn apply_twice(ctx: *rzig.Ctx, f: rzig.Function, x: []const f64) rzig.Error![]const f64 {
const once = try f.call(ctx, []const f64, .{x});
return f.call(ctx, []const f64, .{ .x = once });
}An R error, an interrupt, or a condition handler or restart outside
the call that takes control returns rzig.Error. Return it,
and R continues the error with its class and call once the Zig frames
have run their defer blocks, so tryCatch(),
withCallingHandlers() and traceback() behave
as they would without Zig in between. The calls appear as
f(...), with the name of the R argument. A borrowed result
stays allocated until the exported function returns, and R’s
random-number state passes to R and back when ctx.rng() is
in use. Objectives and integrands of rzig.Optimize and
rzig.Integrate may call R functions.
rzig.Optimize.minimize(ctx, method, &problem, x, options)
runs the optimizers of optim() (.bfgs,
.nelder_mead, .conjugate_gradient,
.lbfgsb with bounds) on a Zig objective:
problem has value(x) and, optionally,
gradient(x, g); without one, central differences use the
step of optim().
rzig.Integrate.integrate(ctx, &f, lower, upper, .{})
integrates f.value(x) as integrate() does,
with infinite limits allowed. Errors of the callbacks are reported when
the routine finishes.
const Problem = struct {
data: []const f64,
pub fn value(self: *const Problem, x: []const f64) f64 {
var total: f64 = 0;
for (self.data) |y| total += (y - x[0]) * (y - x[0]);
return total;
}
};
const x = try ctx.dupe(f64, &.{0.0});
const result = try rzig.Optimize.minimize(ctx, .bfgs, &Problem{ .data = data }, x, .{});rzig.parallelFor distributes an indexed computation
across Zig worker threads. Its state is checked at compile time:
Ctx, Sexp, matrices, list builders, mutable
inputs, attributed results, opaque pointers, and function pointers
cannot cross into a worker.
const Work = struct {
input: []const f64,
output: []f64,
fn square(work: *@This(), index: usize) void {
work.output[index] = work.input[index] * work.input[index];
}
};
pub fn squares(ctx: *rzig.Ctx, input: []const f64) rzig.Error![]f64 {
const output = try ctx.alloc(f64, input.len);
var work = Work{ .input = input, .output = output };
try rzig.parallelFor(ctx, input.len, &work, Work.square);
return output;
}The callback has the exact signature
fn(*State, usize) void and always runs off the R thread. It
must be panic-free and must not import or call R or RZig APIs. If a
worker can fail, record that in atomic plain-data state, wait for
parallelFor to join every worker, and call
rzig.raise on the calling thread.
parallelFor starts one worker thread per core, at most
32 and at most one per index. The environment variable
RZIG_NUM_THREADS lowers that number (for example
Sys.setenv(RZIG_NUM_THREADS = 2) in R),
OMP_THREAD_LIMIT is respected as well, and while
R CMD check limits the cores
(_R_CHECK_LIMIT_CORES_, set for CRAN submissions) at most
two threads run, as the CRAN policy requires.
rzig.parallelThreadCount(count) returns the number of
threads a call would use, for example to size per-thread buffers.
RZig keeps R’s non-local error mechanism at the outer native
boundary. Internal Zig code returns explicit errors, all Zig-owned
cleanup finishes before an R error is raised, and cleanup is protected
if an R API call jumps out. A jump of R out of R code that Zig calls
(rzig.Function) stops before the Zig frames and resumes
after them. Inputs are borrowed read-only, return slices are copied into
R-owned memory, and generated packages build in ReleaseSafe
mode so bounds and overflow checks stay enabled.
R’s API remains single-threaded. Zig code may parallelize pure computation, but must not call R from worker threads.
The package tests run with R CMD check and need no Zig.
The repository test suite runs with Zig 0.16.0 and 0.17.0:
zig build test
zig build --release=safe
zig build lint
R CMD build tests/fixtures/rzigtestContinuous integration builds and checks the fixture package on Linux, macOS, and Windows, and runs GC-stress and memory-analysis profiles where available.
From version 1.0.0, rzig follows semantic versioning for its public
interface: the exported R functions, the declarations of the Zig module
rzig except rzig.internal, the markers and
options of the Zig source (/// @export,
/// @class, @default), the fields of
DESCRIPTION that it reads (Config/rzig/...),
and the files it writes into a package. A change that breaks code using
them comes with a new major version; additions come with minor versions.
rzig.internal, the generated manifest and the framework
files under src/rzig/framework are rzig’s own and change
with any version; update_rzig() replaces them.
Zig changes between release series, so rzig supports the two most recent series it has been tested with (0.16 and 0.17 for rzig 1.0.0). Support for a new series comes with a minor version; a series is dropped in a minor version that NEWS announces one version ahead.
The vignettes and inst/examples/poclp (the case study of
the article on rzig) show further packages.
GPL-3. See COPYING for the full license text. RZig 0.2.3 and earlier releases on CRAN were published under the MIT license.