--- title: "C sources, C libraries and code shared between packages" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{C sources, C libraries and code shared between packages} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(rzig) compiler <- find_zig(required = FALSE) has_zig <- isTRUE(compiler$supported) ``` A package that uses RZig can keep C, C++ or Fortran sources next to its Zig code, compile a C library that it ships, share a Zig module with other packages, and offer C-callable functions to other packages. This vignette shows the four forms. The scaffolds and checks are plain R; the builds run when a supported Zig compiler is found (this copy `r if (has_zig) "ran" else "skipped"` them). ```{r roots} root <- file.path(tempdir(), "sharing") dir.create(root) lib <- file.path(root, "lib") dir.create(lib) header <- c( "const rzig = @import(\"rzig\");", "", "pub const panic = rzig.Panic;", "", "comptime {", " rzig.registerModule(@This());", "}", "" ) ``` ## C, C++ and Fortran sources next to Zig `use_rzig()` stops when `src/` already holds such sources, because the generated build would not compile them. With `mixed = TRUE`, R compiles them with its own compilers next to the Zig library, and `src/entry.c` leaves `R_init_` to the package, whose own init function registers its routines with `rzig_register()`, which adds the Zig routines: ```{r mixed} mixed <- file.path(root, "mixedpkg") dir.create(file.path(mixed, "src"), recursive = TRUE) writeLines(c("Package: mixedpkg", "Version: 0.0.1"), file.path(mixed, "DESCRIPTION")) writeLines(c( "#include ", "SEXP legacy_twice(SEXP x) { return Rf_ScalarReal(2 * Rf_asReal(x)); }" ), file.path(mixed, "src", "legacy.c")) try(use_rzig(mixed)) use_rzig(mixed, mixed = TRUE) writeLines(c( "#include ", "#include ", "SEXP legacy_twice(SEXP x);", "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_twice\", (DL_FUNC) &legacy_twice, 1},", " {NULL, NULL, 0}", "};", "void R_init_mixedpkg(DllInfo *dll) {", " rzig_register(dll, NULL, call_methods, NULL, NULL);", "}" ), file.path(mixed, "src", "init.c")) status <- rzig_status(mixed) status[status$step == "Scaffold", ] ``` ## A C library compiled by Zig A package can ship the sources of a C library 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: ```{r c-library, comment = ""} clib <- file.path(root, "clibpkg") create_rzig_package(clib, tests = FALSE) vendor <- file.path(clib, "src", "rzig", "vendor", "smooth") dir.create(vendor, recursive = TRUE) writeLines("double smooth3(double a, double b, double c);", file.path(vendor, "smooth.h")) writeLines(c( "#include \"smooth.h\"", "double smooth3(double a, double b, double c) { return (a + 2 * b + c) / 4; }" ), file.path(vendor, "smooth.c")) writeLines(c( ".{", " .{", " .name = \"smooth\",", " .header = \"vendor/smooth/smooth.h\",", " .sources = .{\"vendor/smooth/smooth.c\"},", " .include = .{\"vendor/smooth\"},", " },", "}" ), file.path(clib, "src", "rzig", "c_libraries.zon")) writeLines(c(header, "const smooth = @import(\"smooth\");", "", "/// Smooth a vector with the weights 1, 2, 1 of the C library.", "/// @param x A numeric vector.", "/// @return A numeric vector.", "/// @export", "pub fn smoothed(ctx: *rzig.Ctx, x: []const f64) rzig.Error![]f64 {", " const out = try ctx.alloc(f64, x.len);", " for (out, 0..) |*slot, i| {", " const left = x[if (i == 0) 0 else i - 1];", " const right = x[@min(i + 1, x.len - 1)];", " slot.* = smooth.smooth3(left, x[i], right);", " }", " return out;", "}" ), file.path(clib, "src", "rzig", "src", "main.zig")) document(clib) ``` ```{r c-library-build, eval = has_zig} built <- build_zig(clib, lib = lib) built$ok smoothed <- getExportedValue(loadNamespace(built$package, lib.loc = lib), "smoothed") smoothed(c(0, 4, 0, 4)) ``` ## A Zig module shared between packages A package shares Zig code as a module, `inst/zig//root.zig`, which R installs with it. Another package imports the module: `use_rzig_import()` names it in `Config/rzig/imports` and the providing package in `LinkingTo`, so that R installs the provider first. The importing package compiles the module, so the two packages share no code when they run: ```{r modules, comment = ""} provider <- file.path(root, "kernelpkg") create_rzig_package(provider, tests = FALSE) use_rzig_module(provider, "kernels") cat(readLines(file.path(provider, "inst", "zig", "kernels", "root.zig")), sep = "\n") consumer <- file.path(root, "userpkg") create_rzig_package(consumer, tests = FALSE) use_rzig_import(consumer, "kernelpkg", "kernels") writeLines(c(header, "const kernels = @import(\"kernels\");", "", "/// Twice the sum, by the module of kernelpkg.", "/// @param x A numeric vector.", "/// @return A number.", "/// @export", "pub fn twice_sum(x: []const f64) f64 {", " return 2 * kernels.sum(x);", "}" ), file.path(consumer, "src", "rzig", "src", "main.zig")) document(consumer) read.dcf(file.path(consumer, "DESCRIPTION"), fields = c("LinkingTo", "Config/rzig/imports")) ``` ```{r modules-build, eval = has_zig} old_paths <- .libPaths() .libPaths(c(lib, old_paths)) build_zig(provider, lib = lib)$ok consumer_build <- build_zig(consumer, lib = lib) consumer_build$ok twice_sum <- getExportedValue(loadNamespace(consumer_build$package, lib.loc = lib), "twice_sum") twice_sum(c(1, 2, 3)) .libPaths(old_paths) ``` ## C-callable functions Compiled code of other packages 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, which callers check because R checks neither signatures nor versions: ```{r callables, comment = ""} callee <- file.path(root, "calleepkg") create_rzig_package(callee, tests = FALSE) writeLines(c(header, "fn clamp(x: f64, low: f64, high: f64) callconv(.c) f64 {", " return @min(@max(x, low), high);", "}", "", "pub const rzig_callables = .{ .clamp = clamp };", "pub const rzig_interface_version: u32 = 1;", "", "/// The interface version of the C-callable functions.", "/// @return An integer.", "/// @export", "pub fn callee_interface() i32 {", " return 1;", "}" ), file.path(callee, "src", "rzig", "src", "main.zig")) document(callee) caller <- file.path(root, "callerpkg") create_rzig_package(caller, tests = FALSE) writeLines(c(header, "const Clamp = fn (f64, f64, f64) callconv(.c) f64;", "", "/// Clamp values with the function of calleepkg.", "/// @param x A numeric vector.", "/// @return A numeric vector.", "/// @export", "pub fn clamped(ctx: *rzig.Ctx, x: []const f64) rzig.Error![]f64 {", " const clamp = try rzig.callable(ctx, Clamp, \"calleepkg\", \"clamp\", .{ .interface = 1 });", " const out = try ctx.alloc(f64, x.len);", " for (x, out) |value, *slot| slot.* = clamp(value, 0, 1);", " return out;", "}" ), file.path(caller, "src", "rzig", "src", "main.zig")) document(caller) ``` ```{r callables-build, eval = has_zig} old_paths <- .libPaths() .libPaths(c(lib, old_paths)) build_zig(callee, lib = lib)$ok caller_build <- build_zig(caller, lib = lib) caller_build$ok clamped <- getExportedValue(loadNamespace(caller_build$package, lib.loc = lib), "clamped") clamped(c(-1, 0.5, 2)) .libPaths(old_paths) ``` `rzig.callable()` loads the namespace of the providing package, which `R_GetCCallable()` does not do; the calling package lists the provider in `Imports`. ```{r cleanup, include = FALSE} if (has_zig) { for (package in c("callerpkg", "calleepkg", "userpkg", "kernelpkg", "clibpkg")) { if (package %in% loadedNamespaces()) unloadNamespace(package) } } unlink(root, recursive = TRUE) ```