{"id":43200148,"url":"https://github.com/sounkou-bioinfo/rtinycc","last_synced_at":"2026-04-24T21:05:28.597Z","repository":{"id":329412516,"uuid":"1119493482","full_name":"sounkou-bioinfo/Rtinycc","owner":"sounkou-bioinfo","description":"Builds 'TinyCC' 'Cli' and Library for 'C' Scripting in 'R'","archived":false,"fork":false,"pushed_at":"2026-04-15T22:17:49.000Z","size":6215,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-15T23:30:28.634Z","etag":null,"topics":["c","ffi","r","rstats","tinycc"],"latest_commit_sha":null,"homepage":"https://sounkou-bioinfo.github.io/Rtinycc/","language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sounkou-bioinfo.png","metadata":{"files":{"readme":"README.Rmd","changelog":"NEWS.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-19T11:04:47.000Z","updated_at":"2026-04-15T22:15:37.000Z","dependencies_parsed_at":"2026-02-07T21:01:06.490Z","dependency_job_id":null,"html_url":"https://github.com/sounkou-bioinfo/Rtinycc","commit_stats":null,"previous_names":["sounkou-bioinfo/rtinycc"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/sounkou-bioinfo/Rtinycc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sounkou-bioinfo%2FRtinycc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sounkou-bioinfo%2FRtinycc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sounkou-bioinfo%2FRtinycc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sounkou-bioinfo%2FRtinycc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sounkou-bioinfo","download_url":"https://codeload.github.com/sounkou-bioinfo/Rtinycc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sounkou-bioinfo%2FRtinycc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32240620,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T13:21:15.438Z","status":"ssl_error","status_checked_at":"2026-04-24T13:21:15.005Z","response_time":64,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","ffi","r","rstats","tinycc"],"created_at":"2026-02-01T06:03:26.850Z","updated_at":"2026-04-24T21:05:28.587Z","avatar_url":"https://github.com/sounkou-bioinfo.png","language":"R","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\noutput: github_document\n---\n\n\u003c!-- README.md is generated from README.Rmd. Please edit that file --\u003e\n\n```{r, include = FALSE}\nknitr::opts_chunk$set(\n  collapse = TRUE,\n  comment = \"#\u003e\",\n  fig.path = \"man/figures/README-\",\n  out.width = \"100%\"\n)\nhas_callme \u003c- requireNamespace(\"callme\", quietly = TRUE)\nhas_bench \u003c- requireNamespace(\"bench\", quietly = TRUE)\n```\n\n# Rtinycc\n\n Builds `TinyCC` `Cli` and Library For `C` Scripting in `R`\n\n\u003c!-- badges: start --\u003e\n[![R-CMD-check](https://github.com/sounkou-bioinfo/Rtinycc/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/sounkou-bioinfo/Rtinycc/actions/workflows/R-CMD-check.yaml)\n[![CRAN status](https://www.r-pkg.org/badges/version/Rtinycc)](https://CRAN.R-project.org/package=Rtinycc)\n[![Rtinycc status badge](https://sounkou-bioinfo.r-universe.dev/Rtinycc/badges/version)](https://sounkou-bioinfo.r-universe.dev/Rtinycc)\n\u003c!-- badges: end --\u003e\n\n## Abstract\n\nRtinycc is an R interface to [TinyCC](https://github.com/TinyCC/tinycc), providing both CLI access and a libtcc-backed in-memory compiler. It includes an FFI inspired by [Bun's FFI](https://bun.com/docs/runtime/ffi) for binding C symbols with predictable type conversions and pointer utilities. The package works on unix-alikes and Windows and focuses on embedding TinyCC and enabling JIT-compiled bindings directly from R. Combined with [treesitter.c](https://github.com/sounkou-bioinfo/treesitter.c), which provides C header parsers, it can be used to rapidly generate declarative bindings.\n\n\n## How it works\n\nWhen you call `tcc_compile()`, Rtinycc generates C wrapper functions whose\nsignature follows the `.Call` convention (`SEXP` in, `SEXP` out). These wrappers\nconvert R types to C, call the target function, and convert the result back.\nTCC compiles them in-memory -- no shared library is written to disk and no\n`R_init_*` registration is needed.\n\nAfter `tcc_relocate()`, wrapper pointers are retrieved via `tcc_get_symbol()`,\nwhich internally calls `RC_libtcc_get_symbol()`. That function converts TCC's\nraw `void*` into a `DL_FUNC` wrapped with `R_MakeExternalPtrFn` (tagged\n`\"native symbol\"`). On the R side, [`make_callable()`](R/ffi.R) creates a\nclosure that passes this external pointer to `.Call` (aliased as `.RtinyccCall`\nto keep `R CMD check` happy).\n\nThe design follows [CFFI's](https://cffi.readthedocs.io/) API-mode pattern:\ninstead of computing struct layouts and calling conventions in R (ABI-mode,\nlike Python's ctypes), the generated C code lets TCC handle `sizeof`,\n`offsetof`, and argument passing. Rtinycc never replicates platform-specific\nlayout rules. The wrappers can also link against external shared libraries\nwhose symbols TCC resolves at relocation time. For background on how this\ncompares to a libffi approach, see the\n[`RSimpleFFI` README](https://github.com/sounkou-bioinfo/RSimpleFFI#readme).\n\nOn macOS the configure script strips `-flat_namespace` from TCC's build to\navoid [BUS ERROR issues](https://genomic.social/@bioinfhotep/115765645745231377). Without it, TCC cannot resolve host symbols (e.g.\n`RC_free_finalizer`) through the dynamic linker. Rtinycc works around this\nwith `RC_libtcc_add_host_symbols()`, which registers package-internal C\nfunctions via `tcc_add_symbol()` before relocation. Any new C function\nreferenced by generated TCC code must be added there.\n\nOn Windows, the `configure.win` script generates a UCRT-backed `msvcrt.def` so\nTinyCC resolves CRT symbols against `ucrtbase.dll` (R 4.2+ uses UCRT).\n\n\nOwnership semantics are explicit. Pointers from `tcc_malloc()` are tagged `rtinycc_owned` and can be released with `tcc_free()` (or by their R finalizer). Generated struct constructors use a struct-specific tag\n(`struct_\u003cname\u003e`) with an `RC_free_finalizer`; free them with `struct_\u003cname\u003e_free()`, not `tcc_free()`. Pointers from `tcc_data_ptr()` are\ntagged `rtinycc_borrowed` and are never freed by Rtinycc. Array returns are copied into a fresh R vector; set `free = TRUE` only when the C function returns a `malloc`-owned buffer.\n\n## Installation\n\n``` r\ninstall.packages(\n      'Rtinycc', \n        repos = c('https://sounkou-bioinfo.r-universe.dev', \n                  'https://cloud.r-project.org')\n        )\n```\n\n## Usage\n\n### CLI\n\nThe CLI interface compiles C source files to standalone executables using the bundled TinyCC toolchain.\n\n```{r cli}\nlibrary(Rtinycc)\n\nsrc \u003c- system.file(\"c_examples\", \"forty_two.c\", package = \"Rtinycc\")\nexe \u003c- tempfile()\ntcc_run_cli(c(\n  \"-B\", tcc_prefix(),\n  paste0(\"-I\", tcc_include_paths()),\n  paste0(\"-L\", tcc_lib_paths()),\n  src, \"-o\", exe\n))\nSys.chmod(exe, mode = \"0755\")\nsystem2(exe, stdout = TRUE)\n```\n\nFor in-memory workflows, prefer libtcc instead.\n\n### In-memory compilation with libtcc\n\nWe can compile and call C functions entirely in memory. This is the simplest path for quick JIT compilation.\n\n```{r in-memory}\nstate \u003c- tcc_state(output = \"memory\")\ntcc_compile_string(state, \"int forty_two(){ return 42; }\")\ntcc_relocate(state)\ntcc_call_symbol(state, \"forty_two\", return = \"int\")\n```\n\nThe lower-level API gives full control over include paths, libraries, and the R C API. Using `#define _Complex` as a workaround for TCC's lack of [complex type support](https://lists.gnu.org/archive/html/tinycc-devel/2022-04/msg00020.html), we can link against R's headers and call into `libR`.\n\n```{r call-R-C-API}\nstate \u003c- tcc_state(output = \"memory\")\ntcc_add_include_path(state, R.home(\"include\"))\ntcc_add_library_path(state, R.home(\"lib\"))\n\ncode \u003c- '\n#define _Complex\n#include \u003cR.h\u003e\n#include \u003cRinternals.h\u003e\n\ndouble call_r_sqrt(void) {\n  SEXP fn   = PROTECT(Rf_findFun(Rf_install(\"sqrt\"), R_BaseEnv));\n  SEXP val  = PROTECT(Rf_ScalarReal(16.0));\n  SEXP call = PROTECT(Rf_lang2(fn, val));\n  SEXP out  = PROTECT(Rf_eval(call, R_GlobalEnv));\n  double res = REAL(out)[0];\n  UNPROTECT(4);\n  return res;\n}\n'\ntcc_compile_string(state, code)\ntcc_relocate(state)\ntcc_call_symbol(state, \"call_r_sqrt\", return = \"double\")\n```\n\n### Pointer utilities\n\nRtinycc ships a set of typed memory access functions similar to what the [ctypesio](https://cran.r-project.org/package=ctypesio) package offers, but designed around our FFI pointer model. Every scalar C type has a corresponding `tcc_read_*` / `tcc_write_*` pair that operates at a byte offset into any external pointer, so you can walk structs, arrays, and output parameters without writing C helpers.\n\n```{r ffi-utils}\nptr \u003c- tcc_cstring(\"hello\")\ntcc_read_cstring(ptr)\ntcc_read_bytes(ptr, 5)\ntcc_ptr_addr(ptr, hex = TRUE)\ntcc_ptr_is_null(ptr)\ntcc_free(ptr)\n```\n\nTyped reads and writes cover the full scalar range (`i8`/`u8`, `i16`/`u16`, `i32`/`u32`, `i64`/`u64`, `f32`/`f64`) plus pointer dereferencing via `tcc_read_ptr` / `tcc_write_ptr`. All operations use a byte offset and `memcpy` internally for alignment safety.\n\n```{r ffi-typed-rw}\nbuf \u003c- tcc_malloc(32)\ntcc_write_i32(buf, 0L, 42L)\ntcc_write_f64(buf, 8L, pi)\ntcc_read_i32(buf, offset = 0L)\ntcc_read_f64(buf, offset = 8L)\ntcc_free(buf)\n```\n\nPointer-to-pointer workflows are supported for C APIs that return values through output parameters.\n\n```{r ptr-to-ptr}\nptr_ref \u003c- tcc_malloc(.Machine$sizeof.pointer %||% 8L)\ntarget \u003c- tcc_malloc(8)\ntcc_ptr_set(ptr_ref, target)\ntcc_data_ptr(ptr_ref)\ntcc_ptr_set(ptr_ref, tcc_null_ptr())\ntcc_free(target)\ntcc_free(ptr_ref)\n```\n\n## Declarative FFI\n\nA declarative interface inspired by [Bun's FFI](https://bun.com/docs/runtime/ffi) sits on top of the lower-level API. We define types explicitly and Rtinycc generates the binding code, compiling it in memory with TCC.\n\n### Type system\n\nThe FFI exposes a small set of type mappings between R and C. Conversions are explicit and predictable so callers know when data is shared versus copied.\n\nThe scalar type names are C-facing, but the R-side carriers are not all\none-to-one with those C widths:\n\n- `i8`, `i16`, `i32`, `u8`, and `u16` are mediated through R integer scalars\n- `u32`, `i64`, `u64`, `f32`, and `f64` are mediated through R numeric\n  (`double`) coercion and boxing\n- `bool` uses R logical\n- `cstring` uses an R character scalar\n\nThis means `u32` is routed through `double` to preserve the full unsigned\n32-bit range, and `i64` / `u64` are only exact up to `2^53` on the R side.\n\nArray arguments pass R vectors to C with zero copy: `raw` maps to `uint8_t*`, `integer_array` to `int32_t*`, `numeric_array` to `double*`. \n\nPointer types include `ptr` (opaque external pointer), `sexp` (pass a `SEXP` directly), and callback signatures like `callback:double(double)`.\n\nVariadic functions are supported in two forms: typed prefix tails (`varargs`) and\nbounded dynamic tails (`varargs_types` + `varargs_min`/`varargs_max`). Prefix\nmode is the cheaper runtime path because dispatch is by tail arity only;\nbounded dynamic mode adds per-call scalar type inference to select a compatible\nwrapper. For hot loops, prefer fixed arity first, then prefix variadics with a\ntight maximum tail size.\n\n\nArray returns use `returns = list(type = \"integer_array\", length_arg = 2, free = TRUE)` to copy the result into a new R vector. The `length_arg` is the 1-based index of the C argument that carries the array length. Set `free = TRUE` when the C function returns a `malloc`-owned buffer.\n\n### Simple functions\n\n```{r ffi-simple}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source(\"\n    int add(int a, int b) { return a + b; }\n  \") |\u003e\n  tcc_bind(add = list(args = list(\"i32\", \"i32\"), returns = \"i32\")) |\u003e\n  tcc_compile()\n\nffi$add(5L, 3L)\n```\n\n\n### Variadic calls (e.g. `Rprintf` style)\n\nRtinycc supports two ways to bind variadic tails. The legacy approach uses\n`varargs` as a typed prefix tail, while the bounded dynamic approach uses\n`varargs_types` together with `varargs_min` and `varargs_max`. In the bounded\nmode, wrappers are generated across the allowed arity and type combinations,\nand runtime dispatch selects the matching wrapper from the scalar tail values\nprovided at call time.\n\n```{r ffi-variadic-rprintf}\nffi_var \u003c- tcc_ffi() |\u003e\n  tcc_header(\"#include \u003cR_ext/Print.h\u003e\") |\u003e\n  tcc_source('\n    #include \u003cstdarg.h\u003e\n\n    int sum_fmt(int n, ...) {\n      va_list ap;\n      va_start(ap, n);\n      int s = 0;\n      for (int i = 0; i \u003c n; i++) s += va_arg(ap, int);\n      va_end(ap);\n      Rprintf(\"sum_fmt(%d) = %d\\\\n\", n, s);\n      return s;\n    }\n  ') |\u003e\n  tcc_bind(\n    Rprintf = list(\n      args = list(\"cstring\"),\n      variadic = TRUE,\n      varargs_types = list(\"i32\"),\n      varargs_min = 0L,\n      varargs_max = 4L,\n      returns = \"void\"\n    ),\n    sum_fmt = list(\n      args = list(\"i32\"),\n      variadic = TRUE,\n      varargs_types = list(\"i32\"),\n      varargs_min = 0L,\n      varargs_max = 4L,\n      returns = \"i32\"\n    )\n  ) |\u003e\n  tcc_compile()\n\nffi_var$Rprintf(\"Rprintf via bind: %d + %d = %d\\n\", 2L, 3L, 5L)\nffi_var$sum_fmt(0L)\nffi_var$sum_fmt(2L, 10L, 20L)\nffi_var$sum_fmt(4L, 1L, 2L, 3L, 4L)\n```\n\n\n### Linking external libraries\n\nWe can bind directly to symbols in shared libraries. Here we link against `libm`.\n\n```{r ffi-link}\nmath \u003c- tcc_ffi() |\u003e\n  tcc_library(\"m\") |\u003e\n  tcc_bind(\n    sqrt  = list(args = list(\"f64\"), returns = \"f64\"),\n    sin   = list(args = list(\"f64\"), returns = \"f64\"),\n    floor = list(args = list(\"f64\"), returns = \"f64\")\n  ) |\u003e\n  tcc_compile()\n\nmath$sqrt(16.0)\nmath$sin(pi / 2)\nmath$floor(3.7)\n\n\n\n```\n### Compiler options\n\nUse `tcc_options()` to pass raw TinyCC options in the high-level FFI pipeline.\nFor low-level states, use `tcc_set_options()` directly.\n\n```{r ffi-options}\nffi_opt_off \u003c- tcc_ffi() |\u003e\n  tcc_options(\"-O0\") |\u003e\n  tcc_source('\n    int opt_macro() {\n    #ifdef __OPTIMIZE__\n      return 1;\n    #else\n      return 0;\n    #endif\n    }\n  ') |\u003e\n  tcc_bind(opt_macro = list(args = list(), returns = \"i32\")) |\u003e\n  tcc_compile()\n\nffi_opt_on \u003c- tcc_ffi() |\u003e\n  tcc_options(c(\"-Wall\", \"-O2\")) |\u003e\n  tcc_source('\n    int opt_macro() {\n    #ifdef __OPTIMIZE__\n      return 1;\n    #else\n      return 0;\n    #endif\n    }\n  ') |\u003e\n  tcc_bind(opt_macro = list(args = list(), returns = \"i32\")) |\u003e\n  tcc_compile()\n\nffi_opt_off$opt_macro()\nffi_opt_on$opt_macro()\n```\n### Working with arrays\n\nR vectors are passed to C with zero copy. Mutations in C are visible in R.\n\n```{r ffi-arrays}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source(\"\n    #include \u003cstdlib.h\u003e\n    #include \u003cstring.h\u003e\n\n    int64_t sum_array(int32_t* arr, int32_t n) {\n      int64_t s = 0;\n      for (int i = 0; i \u003c n; i++) s += arr[i];\n      return s;\n    }\n\n    void bump_first(int32_t* arr) { arr[0] += 10; }\n\n    int32_t* dup_array(int32_t* arr, int32_t n) {\n      int32_t* out = malloc(sizeof(int32_t) * n);\n      memcpy(out, arr, sizeof(int32_t) * n);\n      return out;\n    }\n  \") |\u003e\n  tcc_bind(\n    sum_array  = list(args = list(\"integer_array\", \"i32\"), returns = \"i64\"),\n    bump_first = list(args = list(\"integer_array\"), returns = \"void\"),\n    dup_array  = list(\n      args = list(\"integer_array\", \"i32\"),\n      returns = list(type = \"integer_array\", length_arg = 2, free = TRUE)\n    )\n  ) |\u003e\n  tcc_compile()\n\nx \u003c- as.integer(1:100) # to avoid ALTREP\n.Internal(inspect(x))\nffi$sum_array(x, length(x))\n\n# Zero-copy: C mutation reflects in R\nffi$bump_first(x)\nx[1]\n\n# Array return: copied into a new R vector, C buffer freed\ny \u003c- ffi$dup_array(x, length(x))\ny[1]\n\n.Internal(inspect(x))\n```\n## Advanced FFI features\n\n### Structs and unions\n\nComplex C types are supported declaratively. Use `tcc_struct()` to generate allocation and accessor helpers. Free instances when done.\n\n```{r struct-example}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source('\n    #include \u003cmath.h\u003e\n    struct point { double x; double y; };\n    double distance(struct point* a, struct point* b) {\n      double dx = a-\u003ex - b-\u003ex, dy = a-\u003ey - b-\u003ey;\n      return sqrt(dx * dx + dy * dy);\n    }\n  ') |\u003e\n  tcc_library(\"m\") |\u003e\n  tcc_struct(\"point\", accessors = c(x = \"f64\", y = \"f64\")) |\u003e\n  tcc_bind(distance = list(args = list(\"ptr\", \"ptr\"), returns = \"f64\")) |\u003e\n  tcc_compile()\n\np1 \u003c- ffi$struct_point_new()\nffi$struct_point_set_x(p1, 0.0)\nffi$struct_point_set_y(p1, 0.0)\n\np2 \u003c- ffi$struct_point_new()\nffi$struct_point_set_x(p2, 3.0)\nffi$struct_point_set_y(p2, 4.0)\n\nffi$distance(p1, p2)\n\nffi$struct_point_free(p1)\nffi$struct_point_free(p2)\n```\n\n### Enums\n\nEnums are exposed as helper functions that return integer constants.\n\n```{r enum-example}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source(\"enum color { RED = 0, GREEN = 1, BLUE = 2 };\") |\u003e\n  tcc_enum(\"color\", constants = c(\"RED\", \"GREEN\", \"BLUE\")) |\u003e\n  tcc_compile()\n\nffi$enum_color_RED()\nffi$enum_color_BLUE()\n```\n\n### Bitfields\n\nBitfields are handled by TCC. Accessors read and write them like normal fields.\n\n```{r bitfield-example}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source(\"\n    struct flags {\n      unsigned int active : 1;\n      unsigned int level  : 4;\n    };\n  \") |\u003e\n  tcc_struct(\"flags\", accessors = c(active = \"u8\", level = \"u8\")) |\u003e\n  tcc_compile()\n\ns \u003c- ffi$struct_flags_new()\nffi$struct_flags_set_active(s, 1L)\nffi$struct_flags_set_level(s, 9L)\nffi$struct_flags_get_active(s)\nffi$struct_flags_get_level(s)\nffi$struct_flags_free(s)\n```\n\n### Global getters and setters\n\nC globals can be exposed with explicit getter/setter helpers.\n\n```{r globals-example}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source(\"\n    int counter = 7;\n    double pi_approx = 3.14159;\n  \") |\u003e\n  tcc_global(\"counter\", \"i32\") |\u003e\n  tcc_global(\"pi_approx\", \"f64\") |\u003e\n  tcc_compile()\n\nffi$global_counter_get()\nffi$global_pi_approx_get()\nffi$global_counter_set(42L)\nffi$global_counter_get()\n```\n\n### Callbacks\n\nR functions can be registered as C function pointers via `tcc_callback()` and passed to compiled code. Specify a `callback:\u003csignature\u003e` argument in `tcc_bind()` so the trampoline is generated automatically. Call `tcc_callback_close()` when you want deterministic invalidation and earlier release of the preserved R function.\n\n```{r callback-example}\ncb \u003c- tcc_callback(function(x) x * x, signature = \"double (*)(double)\")\n\ncode \u003c- '\ndouble apply_fn(double (*fn)(void* ctx, double), void* ctx, double x) {\n  return fn(ctx, x);\n}\n'\n\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source(code) |\u003e\n  tcc_bind(\n    apply_fn = list(\n      args = list(\"callback:double(double)\", \"ptr\", \"f64\"),\n      returns = \"f64\"\n    )\n  ) |\u003e\n  tcc_compile()\n\nffi$apply_fn(cb, tcc_callback_ptr(cb), 7.0)\ntcc_callback_close(cb)\n```\n\n### Callback errors\n\nIf a callback throws an R error, the trampoline catches it, emits a warning, and returns a type-appropriate sentinel instead of unwinding through C. In practice this means NA-like numeric or integer values, `NA` logical, `NULL` for `cstring`, or a null external pointer depending on the declared return type.\n\n```{r callback-error}\ncb_err \u003c- tcc_callback(\n  function(x) stop(\"boom\"),\n  signature = \"double (*)(double)\"\n)\n\nffi_err \u003c- tcc_ffi() |\u003e\n  tcc_source('\n    double call_cb_err(double (*cb)(void* ctx, double), void* ctx, double x) {\n      return cb(ctx, x);\n    }\n  ') |\u003e\n  tcc_bind(\n    call_cb_err = list(\n      args = list(\"callback:double(double)\", \"ptr\", \"f64\"),\n      returns = \"f64\"\n    )\n  ) |\u003e\n  tcc_compile()\n\nwarned \u003c- FALSE\nres \u003c- withCallingHandlers(\n  ffi_err$call_cb_err(cb_err, tcc_callback_ptr(cb_err), 1.0),\n  warning = function(w) {\n    warned \u003c\u003c- TRUE\n    invokeRestart(\"muffleWarning\")\n  }\n)\nlist(warned = warned, result = res)\ntcc_callback_close(cb_err)\n```\n\n### Async callbacks\n\nFor thread-safe scheduling from worker threads, use\n`callback_async:\u003csignature\u003e` in `tcc_bind()`. The async callback queue\nis initialized automatically at package load.\n\nWhen a bound function has any `callback_async:` argument, the generated\nwrapper automatically runs your C function on a new thread while draining\ncallbacks on the main R thread. Your C code doesn't need to know about\ndraining at all — just call the callback as normal and the wrapper handles\nthe rest.\n\n**Void return (fire-and-forget):** the callback is enqueued from any\nthread and executed on the main R thread automatically — on Windows via\nR's message pump, on Linux/macOS via R's event loop `addInputHandler`.\n\n**Non-void return (synchronous):** the worker thread blocks until the\nmain R thread executes the callback and returns the real result.\nSupported return types: integer variants (`int`, `int32_t`, `i8`, `i16`,\n`u8`, `u16`), floating-point (`double`, `float`), `bool`/`logical`, and\npointer (`void*`, `T*`).\n\n```{r callback-async}\n# Fire-and-forget: void callback accumulated from 100 worker threads\nhits \u003c- 0L\ncb_async \u003c- tcc_callback(\n  function(x) { hits \u003c\u003c- hits + x; NULL },\n  signature = \"void (*)(int)\"\n)\n\ncode_async \u003c- '\nstruct task { void (*cb)(void* ctx, int); void* ctx; int value; };\n\n#ifdef _WIN32\n#include \u003cwindows.h\u003e\n\nstatic DWORD WINAPI worker(LPVOID data) {\n  struct task* t = (struct task*) data;\n  t-\u003ecb(t-\u003ectx, t-\u003evalue);\n  return 0;\n}\n\nint spawn_async(void (*cb)(void* ctx, int), void* ctx, int value) {\n  if (!cb || !ctx) return -1;\n  struct task t;\n  t.cb = cb;\n  t.ctx = ctx;\n  t.value = value;\n  HANDLE th = CreateThread(NULL, 0, worker, \u0026t, 0, NULL);\n  if (!th) return -2;\n  WaitForSingleObject(th, INFINITE);\n  CloseHandle(th);\n  return 0;\n}\n#else\n#include \u003cpthread.h\u003e\n\nstatic void* worker(void* data) {\n  struct task* t = (struct task*) data;\n  t-\u003ecb(t-\u003ectx, t-\u003evalue);\n  return NULL;\n}\n\nint spawn_async(void (*cb)(void* ctx, int), void* ctx, int value) {\n  if (!cb || !ctx) return -1;\n  const int n = 100;\n  struct task tasks[100];\n  pthread_t th[100];\n  for (int i = 0; i \u003c n; i++) {\n    tasks[i].cb = cb;\n    tasks[i].ctx = ctx;\n    tasks[i].value = value;\n    if (pthread_create(\u0026th[i], NULL, worker, \u0026tasks[i]) != 0) {\n      for (int j = 0; j \u003c i; j++) pthread_join(th[j], NULL);\n      return -2;\n    }\n  }\n  for (int i = 0; i \u003c n; i++) pthread_join(th[i], NULL);\n  return 0;\n}\n#endif\n'\n\nffi_async \u003c- tcc_ffi() |\u003e\n  tcc_source(code_async)\nif (.Platform$OS.type != \"windows\") {\n  ffi_async \u003c- tcc_library(ffi_async, \"pthread\")\n}\nffi_async \u003c- ffi_async |\u003e\n  tcc_bind(\n    spawn_async = list(\n      args = list(\"callback_async:void(int)\", \"ptr\", \"i32\"),\n      returns = \"i32\"\n    )\n  ) |\u003e\n  tcc_compile()\n\nrc \u003c- ffi_async$spawn_async(cb_async, tcc_callback_ptr(cb_async), 2L)\nhits\ntcc_callback_close(cb_async)\n```\n\nNon-void return works the same way — the generated wrapper handles the\ndrain loop transparently:\n\n```{r callback-async-nonvoid}\ncb_triple \u003c- tcc_callback(\n  function(x) x * 3L,\n  signature = \"int (*)(int)\"\n)\n\n# Pure C: the worker calls the sync callback and returns its result.\n# No drain logic needed — the generated wrapper handles it.\ncode_sync \u003c- '\n#ifdef _WIN32\n#include \u003cwindows.h\u003e\n\nstruct itask { int (*cb)(void*,int); void* ctx; int in; int out; };\n\nstatic DWORD WINAPI iworker(LPVOID p) {\n  struct itask* t = (struct itask*)p;\n  t-\u003eout = t-\u003ecb(t-\u003ectx, t-\u003ein);\n  return 0;\n}\nint run_worker(int (*cb)(void*,int), void* ctx, int x) {\n  struct itask t;\n  t.cb = cb; t.ctx = ctx; t.in = x; t.out = -1;\n  HANDLE th = CreateThread(NULL, 0, iworker, \u0026t, 0, NULL);\n  if (!th) return -1;\n  WaitForSingleObject(th, INFINITE);\n  CloseHandle(th);\n  return t.out;\n}\n#else\n#include \u003cpthread.h\u003e\n\nstruct itask { int (*cb)(void*,int); void* ctx; int in; int out; };\n\nstatic void* iworker(void* p) {\n  struct itask* t = (struct itask*)p;\n  t-\u003eout = t-\u003ecb(t-\u003ectx, t-\u003ein);\n  return NULL;\n}\nint run_worker(int (*cb)(void*,int), void* ctx, int x) {\n  struct itask t;\n  t.cb = cb; t.ctx = ctx; t.in = x; t.out = -1;\n  pthread_t th;\n  if (pthread_create(\u0026th, NULL, iworker, \u0026t) != 0) return -1;\n  pthread_join(th, NULL);\n  return t.out;\n}\n#endif\n'\n\nffi_sync \u003c- tcc_ffi() |\u003e\n  tcc_source(code_sync)\nif (.Platform$OS.type != \"windows\") {\n  ffi_sync \u003c- tcc_library(ffi_sync, \"pthread\")\n}\nffi_sync \u003c- ffi_sync |\u003e\n  tcc_bind(\n    run_worker = list(args = list(\"callback_async:int(int)\", \"ptr\", \"i32\"), returns = \"i32\")\n  ) |\u003e\n  tcc_compile()\n\nffi_sync$run_worker(cb_triple, tcc_callback_ptr(cb_triple), 7L)  # 21\ntcc_callback_close(cb_triple)\n```\n\n### SQLite: a complete example\n\nThis example ties together external library linking, callbacks, and pointer dereferencing. We open an in-memory SQLite database, execute queries, and collect rows through an R callback that reads `char**` arrays using `tcc_read_ptr` and `tcc_read_cstring`.\n\n```{r ffi-sqlite, eval=TRUE}\nptr_size \u003c- .Machine$sizeof.pointer\n\nread_string_array \u003c- function(ptr, n) {\n  vapply(seq_len(n), function(i) {\n    tcc_read_cstring(tcc_read_ptr(ptr, (i - 1L) * ptr_size))\n  }, \"\")\n}\n\ncb \u003c- tcc_callback(\n  function(argc, argv, cols) {\n    values \u003c- read_string_array(argv, argc)\n    names  \u003c- read_string_array(cols, argc)\n    cat(paste(names, values, sep = \" = \", collapse = \", \"), \"\\n\")\n    0L\n  },\n  signature = \"int (*)(int, char **, char **)\"\n)\n\nsqlite \u003c- tcc_ffi() |\u003e\n  tcc_header(\"#include \u003csqlite3.h\u003e\") |\u003e\n  tcc_library(\"sqlite3\") |\u003e\n  tcc_source('\n    void* open_db() {\n      sqlite3* db = NULL;\n      sqlite3_open(\":memory:\", \u0026db);\n      return db;\n    }\n    int close_db(void* db) {\n      return sqlite3_close((sqlite3*)db);\n    }\n  ') |\u003e\n  tcc_bind(\n    open_db  = list(args = list(), returns = \"ptr\"),\n    close_db = list(args = list(\"ptr\"), returns = \"i32\"),\n    sqlite3_libversion = list(args = list(), returns = \"cstring\"),\n    sqlite3_exec = list(\n      args = list(\"ptr\", \"cstring\", \"callback:int(int, char **, char **)\", \"ptr\", \"ptr\"),\n      returns = \"i32\"\n    )\n  ) |\u003e\n  tcc_compile()\n\nsqlite$sqlite3_libversion()\n\ndb \u003c- sqlite$open_db()\nsqlite$sqlite3_exec(db, \"CREATE TABLE t (id INTEGER, name TEXT);\", cb, tcc_callback_ptr(cb), tcc_null_ptr())\nsqlite$sqlite3_exec(db, \"INSERT INTO t VALUES (1, 'hello'), (2, 'world');\", cb, tcc_callback_ptr(cb), tcc_null_ptr())\nsqlite$sqlite3_exec(db, \"SELECT * FROM t;\", cb, tcc_callback_ptr(cb), tcc_null_ptr())\nsqlite$close_db(db)\ntcc_callback_close(cb)\n```\n\n## Header parsing with treesitter.c\n\nFor header-driven bindings, we use `treesitter.c` to parse function signatures and generate binding specifications automatically. For struct, enum, and global helpers, `tcc_generate_bindings()` handles the code generation.\n\nThe default mapper is conservative for pointers: `char*` is treated as `ptr` because C does not guarantee NUL-terminated strings. If you know a parameter is a C string, provide a custom mapper that returns `cstring` for that type.\n\n```{r ffi-treesitter}\nheader \u003c- '\ndouble sqrt(double x);\ndouble sin(double x);\nstruct point { double x; double y; };\nenum status { OK = 0, ERROR = 1 };\nint global_counter;\n'\n\ntcc_treesitter_functions(header)\ntcc_treesitter_structs(header)\ntcc_treesitter_enums(header)\ntcc_treesitter_globals(header)\n\n# Bind parsed functions to libm\nsymbols \u003c- tcc_treesitter_bindings(header)\nmath \u003c- tcc_link(\"m\", symbols = symbols)\nmath$sqrt(16.0)\n\n# Generate struct/enum/global helpers\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source(header) |\u003e\n  tcc_generate_bindings(\n    header,\n    functions = FALSE, structs = TRUE,\n    enums = TRUE, globals = TRUE\n  ) |\u003e\n  tcc_compile()\n\nffi$struct_point_new()\nffi$enum_status_OK()\nffi$global_global_counter_get()\n```\n\n## io_uring Demo\n\n`CSV` parser using [`io_uring`](https://en.wikipedia.org/wiki/Io_uring) on linux\n\n```{r io_uring-demo, eval=TRUE}\nif (Sys.info()[[\"sysname\"]] == \"Linux\") {\n  c_file \u003c- system.file(\"c_examples\", \"io_uring_csv.c\", package = \"Rtinycc\")\n\n  n_rows \u003c- 20000L\n  n_cols \u003c- 8L\n  block_size \u003c- 1024L * 1024L\n\n  set.seed(42)\n  tmp_csv \u003c- tempfile(\"rtinycc_io_uring_readme_\", fileext = \".csv\")\n  on.exit(unlink(tmp_csv), add = TRUE)\n\n  mat \u003c- matrix(runif(n_rows * n_cols), ncol = n_cols)\n  df \u003c- as.data.frame(mat)\n  names(df) \u003c- paste0(\"V\", seq_len(n_cols))\n  utils::write.table(df, file = tmp_csv, sep = \",\", row.names = FALSE, col.names = TRUE, quote = FALSE)\n  csv_size_mb \u003c- as.double(file.info(tmp_csv)$size) / 1024^2\n  message(sprintf(\"CSV size: %.2f MB\", csv_size_mb))\n\n  io_uring_src \u003c- paste(readLines(c_file, warn = FALSE), collapse = \"\\n\")\n\n  ffi \u003c- tcc_ffi() |\u003e\n    tcc_source(io_uring_src) |\u003e\n    tcc_bind(\n      csv_table_read = list(\n        args = list(\"cstring\", \"i32\", \"i32\"),\n        returns = \"sexp\"\n      ),\n      csv_table_io_uring = list(\n        args = list(\"cstring\", \"i32\", \"i32\"),\n        returns = \"sexp\"\n      )\n    ) |\u003e\n    tcc_compile()\n\n  baseline \u003c- utils::read.table(tmp_csv, sep = \",\", header = TRUE)\n  c_tbl \u003c- ffi$csv_table_read(tmp_csv, block_size, n_cols)\n  uring_tbl \u003c- ffi$csv_table_io_uring(tmp_csv, block_size, n_cols)\n  vroom_tbl \u003c- vroom::vroom(\n    tmp_csv,\n    delim = \",\",\n    altrep = FALSE,\n    col_types = vroom::cols(.default = \"d\"),\n    progress = FALSE,\n    show_col_types = FALSE\n  )\n\n  stopifnot(\n    identical(dim(c_tbl), dim(baseline)),\n    identical(dim(uring_tbl), dim(baseline)),\n    identical(dim(vroom_tbl), dim(baseline)),\n    isTRUE(all.equal(c_tbl, baseline, tolerance = 1e-8, check.attributes = FALSE)),\n    isTRUE(all.equal(uring_tbl, baseline, tolerance = 1e-8, check.attributes = FALSE)),\n    isTRUE(all.equal(vroom_tbl, baseline, tolerance = 1e-8, check.attributes = FALSE))\n  )\n\n  timings \u003c- bench::mark(\n    read_table_df = {\n      x \u003c- utils::read.table(tmp_csv, sep = \",\", header = TRUE)\n      nrow(x)\n    },\n    vroom_df_altrep_false = {\n      x \u003c- vroom::vroom(\n        tmp_csv,\n        delim = \",\",\n        altrep = FALSE,\n        col_types = vroom::cols(.default = \"d\"),\n        progress = FALSE,\n        show_col_types = FALSE\n      )\n      nrow(x)\n    },\n    vroom_df_altrep_false_mat = {\n      x \u003c- vroom::vroom(\n        tmp_csv,\n        delim = \",\",\n        altrep = FALSE,\n        col_types = vroom::cols(.default = \"d\"),\n        progress = FALSE,\n        show_col_types = FALSE\n      )\n      x \u003c- as.matrix(x)\n      nrow(x)\n    },\n    c_read_df = {\n      x \u003c- ffi$csv_table_read(tmp_csv, block_size, n_cols)\n      nrow(x)\n    },\n    io_uring_df = {\n      x \u003c- ffi$csv_table_io_uring(tmp_csv, block_size, n_cols)\n      nrow(x)\n    },\n    iterations = 2,\n    memory = TRUE\n  )\n\n  \n  print(timings)\n  \n  plot(timings, type = \"boxplot\") + bench::scale_x_bench_time(base = NULL)\n}\n```\n\n## Known limitations\n\n### `_Complex` types\n\nTCC does not support C99 `_Complex` types. Generated code works around this with `#define _Complex`, which suppresses the keyword. Apply the same workaround in your own `tcc_source()` code when headers pull in complex types.\n\n### 64-bit integer precision\n\nR represents `i64` and `u64` values as `double`, which loses precision beyond $2^{53}$. Values that differ only past that threshold become indistinguishable.\n\n```{r limits-int64}\nsprintf(\"2^53:     %.0f\", 2^53)\nsprintf(\"2^53 + 1: %.0f\", 2^53 + 1)\nidentical(2^53, 2^53 + 1)\n```\n\nFor exact 64-bit arithmetic, keep values in C-allocated storage and manipulate them through pointers.\n\n### Nested structs\n\nNamed nested struct fields can now be declared explicitly with `struct:\u003cname\u003e`.\nGetters return borrowed nested views and setters copy bytes from another struct\nobject of the declared nested type.\n\n```{r limits-nested}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source('\n    struct inner { int a; };\n    struct outer { struct inner in; };\n  ') |\u003e\n  tcc_struct(\"inner\", accessors = c(a = \"i32\")) |\u003e\n  tcc_struct(\"outer\", accessors = list(`in` = \"struct:inner\")) |\u003e\n  tcc_compile()\n\nouter \u003c- ffi$struct_outer_new()\ninner \u003c- ffi$struct_inner_new()\ninner \u003c- ffi$struct_inner_set_a(inner, 42L)\nouter \u003c- ffi$struct_outer_set_in(outer, inner)\ninner_view \u003c- ffi$struct_outer_get_in(outer)\nffi$struct_inner_get_a(inner_view)\nffi$struct_inner_free(inner)\nffi$struct_outer_free(outer)\n```\n\nFor treesitter-generated bindings, nested struct fields inside structs still\nfall back to ptr-like accessors. If you want a borrowed nested view plus a\ncopy-in setter, declare the nested field explicitly with `struct:\u003cname\u003e`.\n\nBitfields are separate from ordinary addressable fields. They use scalar helper\naccessors, but `field_addr()` and `container_of()` reject bitfield members.\n\n```{r bitfield-nested-note, error=TRUE}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source('struct flags { unsigned int flag : 1; };') |\u003e\n  tcc_struct(\n    \"flags\",\n    accessors = list(flag = list(type = \"u8\", bitfield = TRUE, width = 1))\n  )\n\ntcc_field_addr(ffi, \"flags\", \"flag\")\ntcc_container_of(ffi, \"flags\", \"flag\")\n```\n\n### Array fields in structs\n\nArray fields require the `list(type = ..., size = N, array = TRUE)` syntax in `tcc_struct()`, which generates element-wise accessors.\n\n```{r limits-arrays}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source('struct buf { unsigned char data[16]; };') |\u003e\n  tcc_struct(\"buf\", accessors = list(\n    data = list(type = \"u8\", size = 16, array = TRUE)\n  )) |\u003e\n  tcc_compile()\n\nb \u003c- ffi$struct_buf_new()\nffi$struct_buf_set_data_elt(b, 0L, 0xCAL)\nffi$struct_buf_set_data_elt(b, 1L, 0xFEL)\nffi$struct_buf_get_data_elt(b, 0L)\nffi$struct_buf_get_data_elt(b, 1L)\nffi$struct_buf_free(b)\n```\n\n## Serialization and fork safety\n\nCompiled FFI objects are fork-safe: `parallel::mclapply()` and other `fork()`-based parallelism work out of the box because TCC's compiled code lives in memory mappings that survive `fork()` via copy-on-write.\n\nSerialization is also supported. Each `tcc_compiled` object stores its FFI recipe internally, so after `saveRDS()` / `readRDS()` (or `serialize()` / `unserialize()`), the first `$` access detects the dead TCC state pointer and recompiles transparently.\n\n```{r serialize-example}\nffi \u003c- tcc_ffi() |\u003e\n  tcc_source(\"int square(int x) { return x * x; }\") |\u003e\n  tcc_bind(square = list(args = list(\"i32\"), returns = \"i32\")) |\u003e\n  tcc_compile()\n\nffi$square(7L)\n\ntmp \u003c- tempfile(fileext = \".rds\")\nsaveRDS(ffi, tmp)\nffi2 \u003c- readRDS(tmp)\nunlink(tmp)\n\n# Auto-recompiles on first access\nffi2$square(7L)\n```\n\nFor explicit control, use `tcc_recompile()`. Note that raw `tcc_state` objects and bare pointers from `tcc_malloc()` do not carry a recipe and remain dead after deserialization.\n\n## Performance and benchmarking\n\n`Rtinycc` is optimized for fast in-process compilation and convenient FFI\nworkflows, not for winning every microbenchmark against a conventional\nprecompiled `.Call()` shared library.\n\nIn practice, the usual pattern is:\n\n- `Rtinycc` compiles tiny modules very quickly\n- a regular `.Call()` module can have lower minimal per-call overhead\n- array-oriented zero-copy inputs are a much better fit than many tiny scalar\n  crossings\n- return paths that copy native buffers back into fresh R vectors make that\n  copy cost visible\n\nIf you want the benchmark details rather than the high-level summary, see the\n`Compilation and Call Overhead` vignette.\n\n## License\n\nGPL-3\n\n## References\n\n- [TinyCC](https://github.com/TinyCC/tinycc)\n- [Bun's FFI](https://bun.com/docs/runtime/ffi)\n- [CFFI](https://cffi.readthedocs.io/)\n- [RSimpleFFI](https://github.com/sounkou-bioinfo/RSimpleFFI#readme)\n- [CSlug](https://cslug.readthedocs.io/en/latest/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsounkou-bioinfo%2Frtinycc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsounkou-bioinfo%2Frtinycc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsounkou-bioinfo%2Frtinycc/lists"}