R/ffi.R

Defines functions make_callable tcc_compiled_object infer_variadic_arg_type tcc_ffi_compile_state tcc_ffi_create_state tcc_runtime_library_paths tcc_compile tcc_bind tcc_source tcc_header tcc_global tcc_library rtinycc_add_library_file rtinycc_exact_library_name tcc_options tcc_library_path tcc_include tcc_output tcc_ffi tcc_ffi_object as_rtinycc_bound_symbol as_rtinycc_helper_symbol is_rtinycc_bound_symbol rtinycc_nested_struct_type_name rtinycc_is_bitfield_spec rtinycc_struct_field_spec helper_symbol_operation helper_symbol_kind new_rtinycc_bound_symbol new_rtinycc_symbol_return_spec

Documented in tcc_bind tcc_compile tcc_ffi tcc_global tcc_header tcc_include tcc_library tcc_library_path tcc_options tcc_output tcc_source

# Rtinycc - TinyCC for R
# Copyright (C) 2025-2026 Sounkou Mahamane Toure
# SPDX-License-Identifier: GPL-3.0-or-later

# FFI Class and High-Level API
# Bun-style FFI with API mode compilation

# FFI object class
new_rtinycc_symbol_return_spec <- function(
  type,
  type_info,
  length_arg = NULL,
  free = NULL
) {
  structure(
    list(
      type = type,
      type_info = type_info,
      length_arg = length_arg,
      free = free
    ),
    class = c("rtinycc_symbol_return_spec", "rtinycc_symbol_spec_component")
  )
}

new_rtinycc_bound_symbol <- function(
  name,
  args,
  arg_type_info,
  returns,
  return_spec,
  variadic = FALSE,
  varargs = NULL,
  varargs_types = NULL,
  varargs_type_info = NULL,
  varargs_min = NULL,
  varargs_max = NULL,
  varargs_mode = NULL,
  helper_kind = NULL,
  helper_operation = NULL
) {
  classes <- c("rtinycc_bound_symbol", "rtinycc_symbol_spec")
  if (!is.null(helper_kind)) {
    classes <- c(
      paste0("rtinycc_helper_symbol_", helper_kind),
      "rtinycc_helper_symbol",
      classes
    )
  }

  structure(
    list(
      name = name,
      args = args,
      arg_type_info = arg_type_info,
      returns = returns,
      return_spec = return_spec,
      variadic = variadic,
      varargs = varargs,
      varargs_types = varargs_types,
      varargs_type_info = varargs_type_info,
      varargs_min = varargs_min,
      varargs_max = varargs_max,
      varargs_mode = varargs_mode,
      helper_kind = helper_kind,
      helper_operation = helper_operation
    ),
    class = classes
  )
}

helper_symbol_kind <- function(x) {
  if (!inherits(x, "rtinycc_helper_symbol")) {
    stop("Expected rtinycc_helper_symbol object", call. = FALSE)
  }
  x$helper_kind
}

helper_symbol_operation <- function(x) {
  if (!inherits(x, "rtinycc_helper_symbol")) {
    stop("Expected rtinycc_helper_symbol object", call. = FALSE)
  }
  x$helper_operation
}

rtinycc_struct_field_spec <- function(ffi, struct_name, field_name) {
  if (is.null(ffi$structs) || is.null(ffi$structs[[struct_name]])) {
    return(NULL)
  }
  ffi$structs[[struct_name]][[field_name]] %||% NULL
}

rtinycc_is_bitfield_spec <- function(field_spec) {
  is.list(field_spec) && isTRUE(field_spec$bitfield)
}

rtinycc_nested_struct_type_name <- function(field_spec) {
  type_name <- if (is.list(field_spec)) {
    field_spec$type %||% NULL
  } else if (is.character(field_spec) && length(field_spec) == 1) {
    field_spec
  } else {
    NULL
  }
  if (
    is.null(type_name) || !is.character(type_name) || length(type_name) != 1
  ) {
    return(NULL)
  }
  if (!startsWith(type_name, "struct:")) {
    return(NULL)
  }
  sub("^struct:", "", type_name)
}

is_rtinycc_bound_symbol <- function(x) {
  inherits(x, "rtinycc_bound_symbol")
}

as_rtinycc_helper_symbol <- function(
  sym_name,
  sym,
  helper_kind,
  helper_operation
) {
  out <- as_rtinycc_bound_symbol(sym_name, sym)
  new_rtinycc_bound_symbol(
    name = out$name,
    args = out$args,
    arg_type_info = out$arg_type_info,
    returns = out$returns,
    return_spec = out$return_spec,
    variadic = out$variadic,
    varargs = out$varargs,
    varargs_types = out$varargs_types,
    varargs_type_info = out$varargs_type_info,
    varargs_min = out$varargs_min,
    varargs_max = out$varargs_max,
    varargs_mode = out$varargs_mode,
    helper_kind = helper_kind,
    helper_operation = helper_operation
  )
}

as_rtinycc_bound_symbol <- function(sym_name, sym) {
  if (is_rtinycc_bound_symbol(sym)) {
    return(sym)
  }

  if (is.null(sym$args)) {
    sym$args <- list()
  }

  variadic <- isTRUE(sym$variadic)
  has_varargs <- !is.null(sym$varargs)
  has_varargs_types <- !is.null(sym$varargs_types)

  if (
    !is.null(sym$variadic) &&
      (!is.logical(sym$variadic) || length(sym$variadic) != 1)
  ) {
    stop(
      "Symbol '",
      sym_name,
      "' variadic must be TRUE/FALSE",
      call. = FALSE
    )
  }

  if (variadic) {
    if (length(sym$args) == 0) {
      stop(
        "Symbol '",
        sym_name,
        "' variadic functions need at least one fixed argument",
        call. = FALSE
      )
    }

    if (has_varargs && has_varargs_types) {
      stop(
        "Symbol '",
        sym_name,
        "' cannot set both 'varargs' and 'varargs_types'",
        call. = FALSE
      )
    }

    if (!has_varargs && !has_varargs_types) {
      stop(
        "Symbol '",
        sym_name,
        "' variadic functions require 'varargs' or 'varargs_types'",
        call. = FALSE
      )
    }
  } else if (
    has_varargs ||
      has_varargs_types ||
      !is.null(sym$varargs_min) ||
      !is.null(sym$varargs_max)
  ) {
    stop(
      "Symbol '",
      sym_name,
      "' has variadic fields but variadic is not TRUE",
      call. = FALSE
    )
  }

  arg_type_info <- lapply(seq_along(sym$args), function(i) {
    check_ffi_type(
      sym$args[[i]],
      paste0("symbol '", sym_name, "' argument ", i)
    )
  })

  for (i in seq_along(arg_type_info)) {
    type_info <- arg_type_info[[i]]
    if (is_callback_async_type(type_info)) {
      sig <- parse_callback_type(type_info)
      uses_sexp <- identical(trimws(sig$return_type), "SEXP") ||
        identical(trimws(sig$return_type), "sexp") ||
        any(trimws(sig$arg_types) %in% c("SEXP", "sexp"))
      if (uses_sexp) {
        stop(
          "Symbol '",
          sym_name,
          "' async callback argument ",
          i,
          " cannot use SEXP arguments or return type",
          call. = FALSE
        )
      }
    }
  }

  if (!"returns" %in% names(sym)) {
    stop(
      "Symbol '",
      sym_name,
      "' missing 'returns' specification",
      call. = FALSE
    )
  }

  if (is.list(sym$returns)) {
    if (is.null(sym$returns$type)) {
      stop(
        "Symbol '",
        sym_name,
        "' return must include 'type'",
        call. = FALSE
      )
    }
    ret_type <- sym$returns$type
    ret_info <- check_ffi_type(
      ret_type,
      paste0("symbol '", sym_name, "' return")
    )
    if (!is.null(ret_info$kind) && ret_info$kind == "array") {
      if (is.null(sym$returns$length_arg)) {
        stop(
          "Symbol '",
          sym_name,
          "' array return requires 'length_arg'",
          call. = FALSE
        )
      }
      if (
        !is.numeric(sym$returns$length_arg) ||
          length(sym$returns$length_arg) != 1 ||
          is.na(sym$returns$length_arg) ||
          sym$returns$length_arg != as.integer(sym$returns$length_arg)
      ) {
        stop(
          "Symbol '",
          sym_name,
          "' array return length_arg must be a single integer",
          call. = FALSE
        )
      }
      if (!is.null(sym$args) && length(sym$args) > 0) {
        if (
          sym$returns$length_arg < 1 ||
            sym$returns$length_arg > length(sym$args)
        ) {
          stop(
            "Symbol '",
            sym_name,
            "' array return length_arg out of range",
            call. = FALSE
          )
        }
      }
    } else if (!is.null(sym$returns$length_arg) || !is.null(sym$returns$free)) {
      stop(
        "Symbol '",
        sym_name,
        "' non-array return cannot set length_arg/free",
        call. = FALSE
      )
    }
    return_spec <- new_rtinycc_symbol_return_spec(
      type = ret_type,
      type_info = ret_info,
      length_arg = sym$returns$length_arg,
      free = isTRUE(sym$returns$free)
    )
  } else {
    ret_info <- check_ffi_type(
      sym$returns,
      paste0("symbol '", sym_name, "' return")
    )
    return_spec <- new_rtinycc_symbol_return_spec(
      type = sym$returns,
      type_info = ret_info
    )
  }

  varargs_type_info <- NULL
  if (variadic) {
    if (has_varargs_types) {
      if (length(sym$varargs_types) == 0) {
        stop(
          "Symbol '",
          sym_name,
          "' varargs_types must be non-empty",
          call. = FALSE
        )
      }

      varargs_type_info <- lapply(seq_along(sym$varargs_types), function(i) {
        vtype <- sym$varargs_types[[i]]
        vinfo <- check_ffi_type(
          vtype,
          paste0("symbol '", sym_name, "' varargs_types ", i)
        )
        if (!is.null(vinfo$kind) && vinfo$kind != "scalar") {
          stop(
            "Symbol '",
            sym_name,
            "' varargs_types ",
            i,
            " must be a scalar FFI type",
            call. = FALSE
          )
        }
        if (
          ffi_type_family(vinfo) %in%
            c("callback", "callback_async") ||
            identical(vtype, "sexp")
        ) {
          stop(
            "Symbol '",
            sym_name,
            "' varargs_types ",
            i,
            " cannot be callback/sexp",
            call. = FALSE
          )
        }
        vinfo
      })

      if (is.null(sym$varargs_min)) {
        sym$varargs_min <- 0L
      }
      if (is.null(sym$varargs_max)) {
        sym$varargs_max <- sym$varargs_min
      }
      if (
        !is.numeric(sym$varargs_min) ||
          length(sym$varargs_min) != 1 ||
          is.na(sym$varargs_min) ||
          sym$varargs_min < 0 ||
          sym$varargs_min != as.integer(sym$varargs_min)
      ) {
        stop(
          "Symbol '",
          sym_name,
          "' varargs_min must be a non-negative integer",
          call. = FALSE
        )
      }
      if (
        !is.numeric(sym$varargs_max) ||
          length(sym$varargs_max) != 1 ||
          is.na(sym$varargs_max) ||
          sym$varargs_max < 0 ||
          sym$varargs_max != as.integer(sym$varargs_max)
      ) {
        stop(
          "Symbol '",
          sym_name,
          "' varargs_max must be a non-negative integer",
          call. = FALSE
        )
      }
      sym$varargs_min <- as.integer(sym$varargs_min)
      sym$varargs_max <- as.integer(sym$varargs_max)
      if (sym$varargs_min > sym$varargs_max) {
        stop(
          "Symbol '",
          sym_name,
          "' varargs_min must be <= varargs_max",
          call. = FALSE
        )
      }
      sym$varargs_mode <- "types"
      sym$varargs <- NULL
    } else {
      if (length(sym$varargs) == 0) {
        stop(
          "Symbol '",
          sym_name,
          "' variadic functions require non-empty 'varargs' type list",
          call. = FALSE
        )
      }
      max_varargs <- length(sym$varargs)
      if (is.null(sym$varargs_min)) {
        sym$varargs_min <- max_varargs
      }
      if (
        !is.numeric(sym$varargs_min) ||
          length(sym$varargs_min) != 1 ||
          is.na(sym$varargs_min) ||
          sym$varargs_min < 0 ||
          sym$varargs_min > max_varargs ||
          sym$varargs_min != as.integer(sym$varargs_min)
      ) {
        stop(
          "Symbol '",
          sym_name,
          "' varargs_min must be a single integer between 0 and length(varargs)",
          call. = FALSE
        )
      }
      sym$varargs_min <- as.integer(sym$varargs_min)
      sym$varargs_max <- max_varargs
      varargs_type_info <- lapply(seq_along(sym$varargs), function(i) {
        vtype <- sym$varargs[[i]]
        vinfo <- check_ffi_type(
          vtype,
          paste0("symbol '", sym_name, "' vararg ", i)
        )
        if (!is.null(vinfo$kind) && vinfo$kind != "scalar") {
          stop(
            "Symbol '",
            sym_name,
            "' vararg ",
            i,
            " must be a scalar FFI type",
            call. = FALSE
          )
        }
        if (
          ffi_type_family(vinfo) %in%
            c("callback", "callback_async") ||
            identical(vtype, "sexp")
        ) {
          stop(
            "Symbol '",
            sym_name,
            "' vararg ",
            i,
            " cannot be callback/sexp",
            call. = FALSE
          )
        }
        vinfo
      })
      sym$varargs_mode <- "prefix"
      sym$varargs_types <- NULL
    }
  } else {
    sym$varargs_min <- NULL
    sym$varargs_max <- NULL
    sym$varargs_types <- NULL
    sym$varargs_mode <- NULL
  }

  new_rtinycc_bound_symbol(
    name = sym_name,
    args = sym$args,
    arg_type_info = arg_type_info,
    returns = sym$returns,
    return_spec = return_spec,
    variadic = variadic,
    varargs = sym$varargs,
    varargs_types = sym$varargs_types,
    varargs_type_info = varargs_type_info,
    varargs_min = sym$varargs_min,
    varargs_max = sym$varargs_max,
    varargs_mode = sym$varargs_mode
  )
}

tcc_ffi_object <- function() {
  structure(
    list(
      state = NULL,
      symbols = list(),
      headers = character(0),
      c_code = character(0),
      options = character(0),
      libraries = character(0),
      lib_paths = character(0),
      include_paths = character(0),
      output = "memory",
      compiled = FALSE,
      wrapper_symbols = character(0),
      globals = list()
    ),
    class = "tcc_ffi"
  )
}

#' Create a new FFI compilation context
#'
#' Initialize a Bun-style FFI context for API-mode compilation.
#' This is the entry point for the modern FFI API.
#'
#' @return A tcc_ffi object with chaining support
#' @export
#' @examples
#' ffi <- tcc_ffi()
tcc_ffi <- function() {
  tcc_ffi_object()
}

#' Set output type for FFI compilation
#'
#' @param ffi A tcc_ffi object
#' @param output One of "memory", "dll", "exe"
#' @return Updated tcc_ffi object (for chaining)
#' @export
tcc_output <- function(ffi, output = c("memory", "dll", "exe")) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }
  output <- match.arg(output)
  ffi$output <- output
  ffi
}

#' Add include path to FFI context
#'
#' @param ffi A tcc_ffi object
#' @param path Include directory path
#' @return Updated tcc_ffi object (for chaining)
#' @export
tcc_include <- function(ffi, path) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }
  ffi$include_paths <- c(ffi$include_paths, path)
  ffi
}

#' Add library path to FFI context
#'
#' @param ffi A tcc_ffi object
#' @param path Library directory path
#' @return Updated tcc_ffi object (for chaining)
#' @export
tcc_library_path <- function(ffi, path) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }
  ffi$lib_paths <- c(ffi$lib_paths, path)
  ffi
}

#' Add TinyCC compiler options to FFI context
#'
#' Append raw options that are passed to `tcc_set_options()` before compiling
#' generated wrappers (for example `"-O2"` or `"-Wall"`).
#'
#' @param ffi A tcc_ffi object
#' @param options Character vector of option fragments
#' @return Updated tcc_ffi object (for chaining)
#' @export
tcc_options <- function(ffi, options) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }
  if (missing(options)) {
    stop("`options` must be provided", call. = FALSE)
  }
  if (is.null(options)) {
    ffi$options <- character(0)
    return(ffi)
  }

  opts <- as.character(options)
  if (!length(opts)) {
    stop("`options` must not be empty", call. = FALSE)
  }
  opts <- trimws(opts)
  if (any(!nzchar(opts))) {
    stop("`options` entries must be non-empty strings", call. = FALSE)
  }

  ffi$options <- c(ffi$options, opts)
  ffi
}

rtinycc_exact_library_name <- function(path) {
  paste0(":", basename(path))
}

rtinycc_add_library_file <- function(ffi, path) {
  ffi$lib_paths <- c(ffi$lib_paths, dirname(path))
  ffi$libraries <- c(ffi$libraries, rtinycc_exact_library_name(path))
  ffi
}

#' Add library to link against
#'
#' @param ffi A tcc_ffi object
#' @param library Library name (e.g., "m", "sqlite3") or a path to a
#'   shared library (e.g., "libm.so.6"). When a path or platform library file
#'   name is provided, the library directory is added automatically and TinyCC
#'   is asked to link that exact file name. This keeps versioned runtime
#'   libraries such as `libm.so.6` distinct from generic linker names such as
#'   `m`/`libm.so`.
#' @return Updated tcc_ffi object (for chaining)
#' @export
tcc_library <- function(ffi, library) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  libs <- as.character(library)
  for (lib in libs) {
    if (!nzchar(lib)) {
      next
    }

    lib_name <- lib

    # If a path or a file name with extension is provided, resolve it and link
    # the exact file name via TinyCC's -l:<filename> path.  Do not collapse a
    # versioned runtime file such as libm.so.6 to -lm: some systems, including
    # CRAN's Fedora flavor, do not install the unversioned development symlink.
    if (file.exists(lib) || grepl("[/\\\\]", lib)) {
      if (!file.exists(lib)) {
        found_path <- tcc_find_library(lib)
        if (is.null(found_path)) {
          stop("Library not found: ", lib, call. = FALSE)
        }
        lib <- found_path
      }
      ffi <- rtinycc_add_library_file(ffi, lib)
      next
    } else if (grepl("\\.(so|dylib|dll)(\\..*)?$", lib, ignore.case = TRUE)) {
      found_path <- tcc_find_library(lib)
      if (is.null(found_path)) {
        stop("Library not found: ", lib, call. = FALSE)
      }
      ffi <- rtinycc_add_library_file(ffi, found_path)
      next
    }

    if (nzchar(lib_name)) {
      ffi$libraries <- c(ffi$libraries, lib_name)
    }
  }

  ffi
}

#' Declare a global variable getter
#'
#' Register a global C symbol so the compiled object exposes getter/setter
#' functions `global_<name>_get()` and `global_<name>_set()`.
#'
#' @details
#' Globals are limited to scalar FFI types. Array types are rejected.
#' Scalar conversions follow the same rules as wrapper arguments:
#'
#' - Integer inputs (`i8`, `i16`, `i32`, `u8`, `u16`) must be finite and
#'   within range; `NA` values error.
#' - Large integer types (`i64`, `u32`, `u64`) are mediated through R numeric
#'   (double). Values must be integer-valued and within range; for `i64`/`u64`
#'   only exact integers up to $2^53$ are accepted.
#' - Getter wrappers for `i64`/`u64` warn when the stored value exceeds R's
#'   exact integer range for numeric vectors.
#' - `bool` rejects `NA` logicals.
#'
#' Ownership notes:
#'
#' - `ptr` globals store the raw address from an external pointer. If the
#'   external pointer owns memory, keep it alive; otherwise the pointer may
#'   be freed while the global still points to it.
#' - `cstring` globals store a borrowed pointer to R's string data
#'   (UTF-8 translation). Do not free it; for C-owned strings prefer a `ptr`
#'   global and manage lifetime explicitly (e.g., with `tcc_cstring()`).
#'
#' @note
#' Global helpers are generated inside the compiled TCC unit. Recompiling
#' creates a new instance of the global variable; existing compiled objects
#' continue to refer to their own copy.
#'
#' @param ffi A tcc_ffi object
#' @param name Global symbol name
#' @param type FFI type for the global (scalar types only)
#' @return Updated tcc_ffi object (for chaining)
#' @export
#'
#' @examples
#' ffi <- tcc_ffi() |>
#'   tcc_source("int global_counter = 7;") |>
#'   tcc_global("global_counter", "i32") |>
#'   tcc_compile()
#' ffi$global_global_counter_get()
tcc_global <- function(ffi, name, type) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }
  if (!is.character(name) || length(name) != 1 || !nzchar(name)) {
    stop("Global name must be a non-empty string", call. = FALSE)
  }
  if (!is.character(type) || length(type) != 1 || !nzchar(type)) {
    stop("Global type must be a non-empty string", call. = FALSE)
  }

  type_info <- check_ffi_type(type, "global")
  if (!is.null(type_info$kind) && type_info$kind == "array") {
    stop("Global type cannot be an array type", call. = FALSE)
  }
  if (type == "void") {
    stop("Global type cannot be void", call. = FALSE)
  }

  if (is.null(ffi$globals)) {
    ffi$globals <- list()
  }
  ffi$globals[[name]] <- type
  ffi
}

#' Add C headers
#'
#' @param ffi A tcc_ffi object
#' @param header Header string or include directive
#' @return Updated tcc_ffi object (for chaining)
#' @export
tcc_header <- function(ffi, header) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }
  ffi$headers <- c(ffi$headers, header)
  ffi
}

#' Add C source code
#'
#' @param ffi A tcc_ffi object
#' @param code C source code string
#' @return Updated tcc_ffi object (for chaining)
#' @export
tcc_source <- function(ffi, code) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }
  ffi$c_code <- c(ffi$c_code, as.character(code))
  ffi
}

#' Bind symbols with type specifications
#'
#' Define symbols with Bun-style type specifications for API mode.
#' This is the core of the declarative FFI API.
#'
#' @param ffi A tcc_ffi object
#' @param ... Named list of symbol definitions. Each definition is a list with:
#'   \itemize{
#'     \item args: List of fixed FFI argument types (e.g., list("i32", "f64"))
#'     \item returns: FFI type for return value (e.g., "f64", "cstring")
#'     \item variadic: Set TRUE for C varargs functions
#'     \item varargs: Legacy typed variadic tail (exact/prefix mode)
#'     \item varargs_types: Allowed scalar FFI types for true variadic tails
#'     \item varargs_min: Minimum number of trailing varargs
#'     \item varargs_max: Maximum number of trailing varargs (required for
#'       true variadic mode, defaults to \code{varargs_min})
#'     \item code: Optional C code for the symbol (for embedded functions)
#'   }
#'   Callback arguments should use the form \code{callback:<signature>} (e.g.,
#'   \code{callback:double(double)}). The generated trampoline expects a
#'   \code{tcc_callback_ptr(cb)} to the corresponding user-data parameter in
#'   the C API. For thread-safe scheduling, use
#'   \code{callback_async:<signature>} which enqueues the call on the main
#'   thread and returns a default value immediately.
#' @return Updated tcc_ffi object (for chaining)
#' @export
#' @examples
#' ffi <- tcc_ffi() |>
#'   tcc_bind(
#'     add = list(args = list("i32", "i32"), returns = "i32"),
#'     greet = list(args = list("cstring"), returns = "cstring")
#'   )
tcc_bind <- function(ffi, ...) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  symbols <- list(...)

  # Validate and normalize each symbol definition into a classed spec.
  for (sym_name in names(symbols)) {
    ffi$symbols[[sym_name]] <- as_rtinycc_bound_symbol(
      sym_name,
      symbols[[sym_name]]
    )
  }

  ffi
}

#' Compile FFI bindings
#'
#' Compile the defined symbols into callable functions.
#' This generates C wrapper code and compiles it with TinyCC.
#'
#' @param ffi A tcc_ffi object
#' @param verbose Print compilation info
#' @return A tcc_compiled object with callable functions
#' @export
tcc_compile <- function(ffi, verbose = FALSE) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  if (
    length(ffi$symbols) == 0 &&
      length(ffi$structs) == 0 &&
      length(ffi$unions) == 0 &&
      length(ffi$enums) == 0 &&
      length(ffi$globals) == 0
  ) {
    stop(
      "No symbols, structs, unions, enums, or globals defined. Use tcc_bind(), tcc_struct(), tcc_union(), tcc_enum(), or tcc_global() first.",
      call. = FALSE
    )
  }

  # Generate C code (always R API mode now)
  c_code <- generate_ffi_code(
    symbols = ffi$symbols,
    headers = ffi$headers,
    c_code = ffi$c_code,
    is_external = FALSE,
    structs = ffi$structs,
    unions = ffi$unions,
    enums = ffi$enums,
    globals = ffi$globals,
    container_of = ffi$container_of,
    field_addr = ffi$field_addr,
    struct_raw_access = ffi$struct_raw_access,
    introspect = ffi$introspect
  )

  if (verbose) {
    message("Generated C code:\n", c_code)
  }

  state <- tcc_ffi_create_state(ffi, output = ffi$output)
  tcc_ffi_compile_state(
    state = state,
    c_code = c_code,
    libraries = ffi$libraries,
    target = "FFI bindings"
  )

  # Create callable object
  compiled <- tcc_compiled_object(
    state,
    ffi$symbols,
    ffi$output,
    ffi$structs,
    ffi$unions,
    ffi$enums,
    ffi$globals,
    ffi$container_of,
    ffi$field_addr,
    ffi$struct_raw_access,
    ffi$introspect
  )

  # Store the recipe so the object can be recompiled after deserialization
  compiled$.ffi <- ffi

  compiled
}

tcc_runtime_library_paths <- function() {
  paths <- file.path(R.home("lib"))
  if (.Platform$OS.type == "windows") {
    # On Windows, R.dll lives in bin/<arch>; TCC needs it for R API symbols.
    paths <- c(paths, file.path(R.home(), "bin", .Platform$r_arch))
  }
  normalizePath(paths, winslash = "/", mustWork = FALSE)
}

tcc_ffi_create_state <- function(ffi, output = ffi$output) {
  state <- tcc_state(
    output = output,
    include_path = c(
      tcc_include_paths(),
      ffi$include_paths,
      file.path(R.home("include"))
    ),
    lib_path = c(
      tcc_lib_paths(),
      ffi$lib_paths,
      tcc_runtime_library_paths()
    )
  )

  if (!is.null(ffi$options) && length(ffi$options) > 0) {
    rc <- tcc_set_options(state, paste(ffi$options, collapse = " "))
    if (rc < 0) {
      stop("Failed to apply TinyCC compiler options", call. = FALSE)
    }
  }

  state
}

tcc_ffi_compile_state <- function(state, c_code, libraries, target) {
  for (lib in libraries) {
    result <- tcc_add_library(state, lib)
    if (result < 0) {
      stop(
        "Failed to add library '", lib, "' while compiling ", target,
        call. = FALSE
      )
    }
  }

  # Always link against R's shared library so TCC can resolve
  # R API symbols (and any symbols R itself exports).
  tcc_add_library(state, "R")

  result <- tcc_compile_string(state, c_code)
  if (result != 0) {
    stop("Failed to compile ", target, call. = FALSE)
  }

  # Register host symbols for macOS compatibility before relocation.
  .Call(RC_libtcc_add_host_symbols, state)

  result <- tcc_relocate(state)
  if (result != 0) {
    stop("Failed to relocate compiled code for ", target, call. = FALSE)
  }

  invisible(state)
}

infer_variadic_arg_type <- function(x, allowed_types) {
  if (inherits(x, "externalptr")) {
    if ("ptr" %in% allowed_types) {
      return("ptr")
    }
    stop(
      "external pointer variadic argument requires allowed type 'ptr'",
      call. = FALSE
    )
  }

  if (is.character(x) && length(x) == 1) {
    if ("cstring" %in% allowed_types) {
      return("cstring")
    }
    stop(
      "character variadic argument requires allowed type 'cstring'",
      call. = FALSE
    )
  }

  if (is.logical(x) && length(x) == 1) {
    if ("bool" %in% allowed_types) {
      return("bool")
    }
    stop(
      "logical variadic argument requires allowed type 'bool'",
      call. = FALSE
    )
  }

  if (is.raw(x) && length(x) == 1) {
    if ("u8" %in% allowed_types) {
      return("u8")
    }
    if ("i8" %in% allowed_types) {
      return("i8")
    }
    stop(
      "raw variadic argument requires allowed type 'u8' or 'i8'",
      call. = FALSE
    )
  }

  if (is.integer(x) && length(x) == 1) {
    pref <- c(
      "i32",
      "i64",
      "u32",
      "u64",
      "i16",
      "u16",
      "i8",
      "u8",
      "f64",
      "f32"
    )
    pick <- pref[pref %in% allowed_types]
    if (length(pick) > 0) {
      return(pick[[1]])
    }
    stop(
      "integer variadic argument has no compatible allowed type",
      call. = FALSE
    )
  }

  if (is.double(x) && length(x) == 1) {
    pref <- c("f64", "f32")
    pick <- pref[pref %in% allowed_types]
    if (length(pick) > 0) {
      return(pick[[1]])
    }
    stop(
      "numeric variadic argument has no compatible allowed floating type",
      call. = FALSE
    )
  }

  stop(
    "Unsupported variadic argument type; expected scalar logical/integer/numeric/character/raw/externalptr",
    call. = FALSE
  )
}

# Create compiled object with callable functions
tcc_compiled_object <- function(
  state,
  symbols,
  output,
  structs = NULL,
  unions = NULL,
  enums = NULL,
  globals = NULL,
  container_of = NULL,
  field_addr = NULL,
  struct_raw_access = NULL,
  introspect = NULL
) {
  # Build helper names for struct/union/enum helpers
  helper_names <- character()
  helper_specs <- list()

  # Struct helpers
  if (!is.null(structs)) {
    for (struct_name in names(structs)) {
      fields <- structs[[struct_name]]
      # new, free
      helper_names <- c(
        helper_names,
        paste0("struct_", struct_name, "_new"),
        paste0("struct_", struct_name, "_free")
      )
      helper_specs[[paste0("struct_", struct_name, "_new")]] <- list(
        args = list(),
        returns = "sexp"
      )
      helper_specs[[paste0("struct_", struct_name, "_free")]] <- list(
        args = list("sexp"),
        returns = "sexp"
      )
      # getters and setters
      for (field_name in names(fields)) {
        field_spec <- fields[[field_name]]
        is_array <- is.list(field_spec) && isTRUE(field_spec$array)

        helper_names <- c(
          helper_names,
          paste0("struct_", struct_name, "_get_", field_name)
        )
        helper_specs[[paste0(
          "struct_",
          struct_name,
          "_get_",
          field_name
        )]] <- c(
          list(
            args = list("sexp"),
            returns = "sexp"
          ),
          if (rtinycc_is_bitfield_spec(field_spec)) {
            list(.helper_operation = "bitfield_getter")
          } else if (!is.null(rtinycc_nested_struct_type_name(field_spec))) {
            list(.helper_operation = "nested_view")
          } else {
            list()
          }
        )

        if (is_array) {
          helper_names <- c(
            helper_names,
            paste0("struct_", struct_name, "_get_", field_name, "_elt"),
            paste0("struct_", struct_name, "_set_", field_name, "_elt")
          )
          helper_specs[[paste0(
            "struct_",
            struct_name,
            "_get_",
            field_name,
            "_elt"
          )]] <- list(
            args = list("sexp", "i32"),
            returns = "sexp"
          )
          helper_specs[[paste0(
            "struct_",
            struct_name,
            "_set_",
            field_name,
            "_elt"
          )]] <- list(
            args = list("sexp", "i32", "sexp"),
            returns = "sexp"
          )
        } else {
          helper_names <- c(
            helper_names,
            paste0("struct_", struct_name, "_set_", field_name)
          )
          helper_specs[[paste0(
            "struct_",
            struct_name,
            "_set_",
            field_name
          )]] <- c(
            list(
              args = list("sexp", "sexp"),
              returns = "sexp"
            ),
            if (rtinycc_is_bitfield_spec(field_spec)) {
              list(.helper_operation = "bitfield_setter")
            } else if (!is.null(rtinycc_nested_struct_type_name(field_spec))) {
              list(.helper_operation = "nested_setter")
            } else {
              list()
            }
          )
        }
      }
      # container_of helpers
      if (!is.null(container_of) && struct_name %in% names(container_of)) {
        for (member_name in container_of[[struct_name]]) {
          helper_names <- c(
            helper_names,
            paste0("struct_", struct_name, "_from_", member_name)
          )
          helper_specs[[paste0(
            "struct_",
            struct_name,
            "_from_",
            member_name
          )]] <- list(
            args = list("sexp"),
            returns = "sexp"
          )
        }
      }
      # field_addr helpers
      if (!is.null(field_addr) && struct_name %in% names(field_addr)) {
        for (field_name in field_addr[[struct_name]]) {
          helper_names <- c(
            helper_names,
            paste0("struct_", struct_name, "_", field_name, "_addr")
          )
          helper_specs[[paste0(
            "struct_",
            struct_name,
            "_",
            field_name,
            "_addr"
          )]] <- list(
            args = list("sexp"),
            returns = "sexp"
          )
        }
      }
      # raw_access helpers
      if (!is.null(struct_raw_access) && struct_name %in% struct_raw_access) {
        helper_names <- c(
          helper_names,
          paste0("struct_", struct_name, "_get_raw"),
          paste0("struct_", struct_name, "_set_raw")
        )
        helper_specs[[paste0("struct_", struct_name, "_get_raw")]] <- list(
          args = list("sexp", "i32"),
          returns = "sexp"
        )
        helper_specs[[paste0("struct_", struct_name, "_set_raw")]] <- list(
          args = list("sexp", "raw"),
          returns = "sexp"
        )
      }
      # introspection helpers
      if (!is.null(introspect) && introspect) {
        helper_names <- c(
          helper_names,
          paste0("struct_", struct_name, "_sizeof"),
          paste0("struct_", struct_name, "_alignof")
        )
        helper_specs[[paste0("struct_", struct_name, "_sizeof")]] <- list(
          args = list(),
          returns = "sexp"
        )
        helper_specs[[paste0("struct_", struct_name, "_alignof")]] <- list(
          args = list(),
          returns = "sexp"
        )
      }
    }
  }

  # Union helpers
  if (!is.null(unions)) {
    for (union_name in names(unions)) {
      union_def <- unions[[union_name]]
      # new, free
      helper_names <- c(
        helper_names,
        paste0("union_", union_name, "_new"),
        paste0("union_", union_name, "_free")
      )
      helper_specs[[paste0("union_", union_name, "_new")]] <- list(
        args = list(),
        returns = "sexp"
      )
      helper_specs[[paste0("union_", union_name, "_free")]] <- list(
        args = list("sexp"),
        returns = "sexp"
      )
      # getters and setters for members
      for (mem_name in names(union_def$members)) {
        mem_spec <- union_def$members[[mem_name]]
        is_nested_struct <- is.list(mem_spec) &&
          identical(mem_spec$type %||% NULL, "struct")

        helper_names <- c(
          helper_names,
          paste0("union_", union_name, "_get_", mem_name)
        )
        helper_specs[[paste0("union_", union_name, "_get_", mem_name)]] <- c(
          list(
            args = list("sexp"),
            returns = "sexp"
          ),
          if (is_nested_struct) {
            list(.helper_operation = "nested_view")
          } else {
            list()
          }
        )

        if (!is_nested_struct) {
          helper_names <- c(
            helper_names,
            paste0("union_", union_name, "_set_", mem_name)
          )
          helper_specs[[paste0(
            "union_",
            union_name,
            "_set_",
            mem_name
          )]] <- list(
            args = list("sexp", "sexp"),
            returns = "sexp"
          )
        }
      }
      # introspection
      if (!is.null(introspect) && introspect) {
        helper_names <- c(
          helper_names,
          paste0("union_", union_name, "_sizeof"),
          paste0("union_", union_name, "_alignof")
        )
        helper_specs[[paste0("union_", union_name, "_sizeof")]] <- list(
          args = list(),
          returns = "sexp"
        )
        helper_specs[[paste0("union_", union_name, "_alignof")]] <- list(
          args = list(),
          returns = "sexp"
        )
      }
    }
  }

  # Enum helpers
  if (!is.null(enums)) {
    for (enum_name in names(enums)) {
      enum_def <- enums[[enum_name]]
      # introspection
      if (!is.null(introspect) && introspect) {
        helper_names <- c(helper_names, paste0("enum_", enum_name, "_sizeof"))
        helper_specs[[paste0("enum_", enum_name, "_sizeof")]] <- list(
          args = list(),
          returns = "sexp"
        )
      }
      # constant export
      if (!is.null(enum_def$constants)) {
        for (const_name in enum_def$constants) {
          helper_names <- c(
            helper_names,
            paste0("enum_", enum_name, "_", const_name)
          )
          helper_specs[[paste0("enum_", enum_name, "_", const_name)]] <- list(
            args = list(),
            returns = "sexp",
            .helper_operation = "constant"
          )
        }
      }
    }
  }

  # Global helpers
  if (!is.null(globals) && length(globals) > 0) {
    for (global_name in names(globals)) {
      helper_names <- c(
        helper_names,
        paste0("global_", global_name, "_get"),
        paste0("global_", global_name, "_set")
      )
      helper_specs[[paste0("global_", global_name, "_get")]] <- list(
        args = list(),
        returns = "sexp"
      )
      helper_specs[[paste0("global_", global_name, "_set")]] <- list(
        args = list("sexp"),
        returns = "sexp"
      )
    }
  }

  if (length(helper_specs) > 0) {
    helper_specs <- stats::setNames(
      lapply(names(helper_specs), function(sym_name) {
        helper_kind <- if (startsWith(sym_name, "struct_")) {
          "struct"
        } else if (startsWith(sym_name, "union_")) {
          "union"
        } else if (startsWith(sym_name, "enum_")) {
          "enum"
        } else if (startsWith(sym_name, "global_")) {
          "global"
        } else {
          "helper"
        }
        helper_operation <- helper_specs[[sym_name]]$.helper_operation %||%
          if (grepl("_new$", sym_name)) {
            "constructor"
          } else if (grepl("_free$", sym_name)) {
            "destructor"
          } else if (grepl("_get_raw$", sym_name)) {
            "raw_get"
          } else if (grepl("_set_raw$", sym_name)) {
            "raw_set"
          } else if (
            grepl("_sizeof$", sym_name) || grepl("_alignof$", sym_name)
          ) {
            "introspection"
          } else if (grepl("_from_", sym_name, fixed = TRUE)) {
            "container_of"
          } else if (grepl("_addr$", sym_name)) {
            "field_addr"
          } else if (grepl("_set_.*_elt$", sym_name)) {
            "array_setter"
          } else if (grepl("_get_.*_elt$", sym_name)) {
            "array_getter"
          } else if (grepl("_set$", sym_name)) {
            "setter"
          } else if (grepl("_get$", sym_name)) {
            "getter"
          } else if (grepl("_set_", sym_name, fixed = TRUE)) {
            "setter"
          } else if (grepl("_get_", sym_name, fixed = TRUE)) {
            "getter"
          } else {
            "helper"
          }
        as_rtinycc_helper_symbol(
          sym_name,
          helper_specs[[sym_name]],
          helper_kind,
          helper_operation
        )
      }),
      names(helper_specs)
    )
  }

  # Create environment with callable functions
  env <- new.env(parent = emptyenv())
  bind_failures <- character(0)
  add_bind_failure <- function(msg) {
    bind_failures <<- c(bind_failures, msg)
  }

  for (sym_name in names(symbols)) {
    sym <- symbols[[sym_name]]

    if (isTRUE(sym$variadic)) {
      vararg_mode <- sym$varargs_mode %||% "prefix"
      fn_ptrs <- list()

      if (identical(vararg_mode, "types")) {
        allowed_types <- sym$varargs_types %||% list()
        min_varargs <- sym$varargs_min %||% 0L
        max_varargs <- sym$varargs_max %||% min_varargs

        for (n_varargs in seq.int(min_varargs, max_varargs)) {
          type_sequences <- generate_variadic_type_sequences(
            allowed_types,
            n_varargs
          )
          for (this_types in type_sequences) {
            wrapper_name <- variadic_wrapper_name_types(
              paste0("R_wrap_", sym_name),
              this_types
            )
            key <- variadic_signature_key(this_types)

            tryCatch(
              {
                fn_ptr <- tcc_get_symbol(state, wrapper_name)
                if (!tcc_symbol_is_valid(fn_ptr)) {
                  add_bind_failure(
                    paste0(
                      "Symbol '",
                      sym_name,
                      "' returned invalid pointer for '",
                      wrapper_name,
                      "'"
                    )
                  )
                  next
                }
                fn_ptrs[[key]] <- fn_ptr
              },
              error = function(e) {
                add_bind_failure(
                  paste0(
                    "Could not bind symbol '",
                    sym_name,
                    "' wrapper '",
                    wrapper_name,
                    "': ",
                    conditionMessage(e)
                  )
                )
              }
            )
          }
        }
      } else {
        max_varargs <- length(sym$varargs %||% list())
        min_varargs <- sym$varargs_min %||% max_varargs

        for (n_varargs in seq.int(min_varargs, max_varargs)) {
          wrapper_name <- variadic_wrapper_name(
            paste0("R_wrap_", sym_name),
            n_varargs
          )

          tryCatch(
            {
              fn_ptr <- tcc_get_symbol(state, wrapper_name)
              if (!tcc_symbol_is_valid(fn_ptr)) {
                add_bind_failure(
                  paste0(
                    "Symbol '",
                    sym_name,
                    "' returned invalid pointer for '",
                    wrapper_name,
                    "'"
                  )
                )
                next
              }
              fn_ptrs[[as.character(n_varargs)]] <- fn_ptr
            },
            error = function(e) {
              add_bind_failure(
                paste0(
                  "Could not bind symbol '",
                  sym_name,
                  "' wrapper '",
                  wrapper_name,
                  "': ",
                  conditionMessage(e)
                )
              )
            }
          )
        }
      }

      if (length(fn_ptrs) == 0) {
        add_bind_failure(
          paste0(
            "Could not bind any variadic wrappers for symbol '",
            sym_name,
            "'"
          )
        )
        next
      }

      env[[sym_name]] <- make_callable(fn_ptrs, sym, state)
      next
    }

    wrapper_name <- paste0("R_wrap_", sym_name)
    tryCatch(
      {
        fn_ptr <- tcc_get_symbol(state, wrapper_name)

        # Validate the pointer before creating callable
        if (!tcc_symbol_is_valid(fn_ptr)) {
          add_bind_failure(
            paste0(
              "Symbol '",
              sym_name,
              "' returned invalid pointer for '",
              wrapper_name,
              "'"
            )
          )
          next
        }
        env[[sym_name]] <- make_callable(fn_ptr, sym, state)
      },
      error = function(e) {
        add_bind_failure(
          paste0(
            "Could not bind symbol '",
            sym_name,
            "': ",
            conditionMessage(e)
          )
        )
      }
    )
  }

  for (sym_name in helper_names) {
    wrapper_name <- paste0("R_wrap_", sym_name)
    sym <- helper_specs[[sym_name]]
    if (is.null(sym)) {
      add_bind_failure(paste0("Unknown helper symbol '", sym_name, "'"))
      next
    }

    tryCatch(
      {
        fn_ptr <- tcc_get_symbol(state, wrapper_name)

        if (!tcc_symbol_is_valid(fn_ptr)) {
          add_bind_failure(
            paste0(
              "Symbol '",
              sym_name,
              "' returned invalid pointer for '",
              wrapper_name,
              "'"
            )
          )
          next
        }

        env[[sym_name]] <- make_callable(fn_ptr, sym, state)
      },
      error = function(e) {
        add_bind_failure(
          paste0(
            "Could not bind symbol '",
            sym_name,
            "': ",
            conditionMessage(e)
          )
        )
      }
    )
  }

  if (length(bind_failures) > 0) {
    stop(
      "Failed to bind compiled wrapper symbols:\n  - ",
      paste(unique(bind_failures), collapse = "\n  - "),
      call. = FALSE
    )
  }

  # Add state and metadata
  env$.state <- state
  env$.symbols <- symbols
  env$.output <- output
  env$.helpers <- list(
    structs = structs,
    unions = unions,
    enums = enums,
    globals = globals
  )
  env$.helper_specs <- helper_specs
  env$.valid <- TRUE

  structure(env, class = "tcc_compiled")
}

# Make a callable function from symbol pointer
# Uses .Call() directly with the external pointer - R can call external pointers!
make_callable <- function(fn_ptr, sym, state) {
  # Force evaluation to ensure pointer is captured
  force(fn_ptr)
  force(sym)
  force(state)

  arg_types <- sym$args %||% list()
  vararg_types <- sym$varargs %||% list()
  vararg_types_allowed <- sym$varargs_types %||% list()
  variadic <- isTRUE(sym$variadic)
  vararg_mode <- if (variadic) sym$varargs_mode %||% "prefix" else NULL
  fixed_n <- length(arg_types)
  varargs_min <- if (variadic) {
    sym$varargs_min %||% length(vararg_types)
  } else {
    0L
  }
  varargs_max <- if (variadic) {
    sym$varargs_max %||% length(vararg_types)
  } else {
    0L
  }
  min_n <- fixed_n + varargs_min
  max_n <- fixed_n + varargs_max
  fn_ptr_is_map <- is.list(fn_ptr)
  prefix_ptr_by_tail <- NULL
  types_ptr_env <- NULL
  if (variadic && fn_ptr_is_map) {
    if (identical(vararg_mode, "prefix")) {
      ptr_names <- suppressWarnings(as.integer(names(fn_ptr)))
      if (
        length(ptr_names) == length(fn_ptr) &&
          length(ptr_names) > 0L &&
          all(!is.na(ptr_names))
      ) {
        prefix_ptr_by_tail <- vector("list", varargs_max + 1L)
        for (i in seq_along(fn_ptr)) {
          tail_n <- ptr_names[[i]]
          if (!is.na(tail_n) && tail_n >= 0L && tail_n <= varargs_max) {
            prefix_ptr_by_tail[[tail_n + 1L]] <- fn_ptr[[i]]
          }
        }
      }
    } else if (identical(vararg_mode, "types")) {
      ptr_keys <- names(fn_ptr)
      if (!is.null(ptr_keys) && length(ptr_keys) == length(fn_ptr)) {
        types_ptr_env <- list2env(fn_ptr, hash = TRUE, parent = emptyenv())
      }
    }
  }
  sym_name <- sym$name

  if (!variadic) {
    expected_n <- fixed_n
    dot_syms <- if (expected_n > 0L) {
      lapply(seq_len(expected_n), function(i) as.name(paste0("..", i)))
    } else {
      list()
    }
    call_expr <- as.call(c(
      as.name(".RtinyccCall"),
      as.name(".call_ptr"),
      dot_syms
    ))
    body_expr <- bquote({
      n_args <- nargs()
      if (n_args != .(expected_n)) {
        stop(
          "Expected ",
          .(expected_n),
          " arguments, got ",
          n_args,
          call. = FALSE
        )
      }
      if (!tcc_symbol_is_valid(.call_ptr)) {
        stop(
          "Function pointer for '",
          .(sym_name),
          "' is no longer valid",
          call. = FALSE
        )
      }
      .(call_expr)
    })
    f <- eval(
      call("function", as.pairlist(alist(... = )), body_expr),
      envir = list2env(
        list(
          .call_ptr = fn_ptr,
          .RtinyccCall = .RtinyccCall,
          tcc_symbol_is_valid = tcc_symbol_is_valid
        ),
        parent = baseenv()
      )
    )

    # Store the pointer in the function's environment to prevent GC
    environment(f)$.fn_ptr <- fn_ptr
    environment(f)$.state <- state
    environment(f)$.arg_types <- arg_types
    environment(f)$.sym_name <- sym_name

    return(f)
  }

  # Create function that calls the wrapper via .Call()
  f <- function(...) {
    args <- list(...)
    n_args <- length(args)
    if (n_args < min_n || n_args > max_n) {
      stop(
        "Expected between ",
        min_n,
        " and ",
        max_n,
        " arguments, got ",
        n_args,
        call. = FALSE
      )
    }
    n_tail <- n_args - fixed_n

    if (identical(vararg_mode, "types")) {
      inferred_types <- character(n_tail)
      if (n_tail > 0L) {
        for (i in seq_len(n_tail)) {
          inferred_types[[i]] <- infer_variadic_arg_type(
            args[[fixed_n + i]],
            vararg_types_allowed
          )
        }
      }
      key <- variadic_signature_key(inferred_types)
      if (!is.null(types_ptr_env)) {
        call_ptr <- if (exists(key, envir = types_ptr_env, inherits = FALSE)) {
          types_ptr_env[[key]]
        } else {
          NULL
        }
      } else {
        call_ptr <- if (fn_ptr_is_map) fn_ptr[[key]] else fn_ptr
      }
    } else {
      if (!is.null(prefix_ptr_by_tail)) {
        idx <- n_tail + 1L
        call_ptr <- if (idx > 0L && idx <= length(prefix_ptr_by_tail)) {
          prefix_ptr_by_tail[[idx]]
        } else {
          NULL
        }
      } else {
        call_ptr <- if (fn_ptr_is_map) {
          fn_ptr[[as.character(n_tail)]]
        } else {
          fn_ptr
        }
      }
    }

    if (is.null(call_ptr)) {
      stop(
        "No compiled variadic wrapper for symbol '",
        sym_name,
        "' and the provided argument shape",
        call. = FALSE
      )
    }

    # Validate pointer is still valid before calling
    if (!tcc_symbol_is_valid(call_ptr)) {
      stop(
        "Function pointer for '",
        sym_name,
        "' is no longer valid",
        call. = FALSE
      )
    }

    # R's .Call() can invoke external pointers as functions.
    rtinycc_call(n_args, call_ptr, args)
  }

  # Store the pointer in the function's environment to prevent GC
  environment(f)$.fn_ptr <- fn_ptr
  environment(f)$.state <- state
  environment(f)$.arg_types <- arg_types
  environment(f)$.sym_name <- sym_name

  f
}

#' Print tcc_ffi object
#'
#' @param x A tcc_ffi object
#' @param ... Ignored
#' @return The input `tcc_ffi` object, invisibly. Called for its side
#'   effect of printing the configured output mode, registered symbols,
#'   and selected libraries/include paths.
#' @export
print.tcc_ffi <- function(x, ...) {
  cat("<tcc_ffi>\n")
  cat("  Output:", x$output, "\n")
  cat("  Symbols:", length(x$symbols), "defined\n")
  if (length(x$symbols) > 0) {
    for (name in names(x$symbols)) {
      sym <- x$symbols[[name]]
      args <- paste(sym$args %||% "void", collapse = ", ")
      cat("    ", name, "(", args, ") -> ", sym$returns, "\n", sep = "")
    }
  }
  if (length(x$libraries) > 0) {
    cat("  Libraries:", paste(x$libraries, collapse = ", "), "\n")
  }
  if (length(x$include_paths) > 0) {
    cat("  Include paths:", length(x$include_paths), "\n")
  }
  invisible(x)
}

#' Print tcc_compiled object
#'
#' @param x A tcc_compiled object
#' @param ... Ignored
#' @return The input `tcc_compiled` object, invisibly. Called for its side
#'   effect of printing the compilation output mode and the status of
#'   compiled callable symbols.
#' @export
print.tcc_compiled <- function(x, ...) {
  cat("<tcc_compiled>\n")
  cat("  Output:", x$.output, "\n")
  cat("  Symbols:", length(x$.symbols), "compiled\n")
  if (length(x$.symbols) > 0) {
    for (name in names(x$.symbols)) {
      if (exists(name, envir = x, inherits = FALSE)) {
        cat("    ", name, "() [callable]\n", sep = "")
      } else {
        cat("    ", name, "() [failed]\n", sep = "")
      }
    }
  }
  invisible(x)
}

#' Access a compiled FFI symbol
#'
#' Overrides \code{$} to detect dead pointers after deserialization
#' and recompile transparently from the stored recipe.
#'
#' @param x A tcc_compiled object
#' @param name Symbol name to access
#' @return The callable function or metadata field
#' @export
`$.tcc_compiled` <- function(x, name) {
  # Fast path: internal fields are always accessible
  if (startsWith(name, ".")) {
    return(.subset2(x, name))
  }

  # Detect dead pointers (nil after deserialization) and recompile.
  # tcc_symbol_is_valid() is a single C call; negligible overhead.
  state <- .subset2(x, ".state")
  if (is.null(state) || !tcc_symbol_is_valid(state)) {
    message("[Rtinycc] Recompiling FFI bindings after deserialization")
    recompile_into(x)
  }

  .subset2(x, name)
}

#' Recompile a tcc_compiled object
#'
#' Explicitly recompile from the stored FFI recipe.
#' Useful after deserialization (\code{readRDS}, \code{unserialize})
#' or to force a fresh compilation.
#'
#' @param compiled A tcc_compiled object
#' @return The recompiled tcc_compiled object (invisibly, same environment)
#' @export
tcc_recompile <- function(compiled) {
  if (!inherits(compiled, "tcc_compiled")) {
    stop("Expected tcc_compiled object", call. = FALSE)
  }
  recompile_into(compiled)
  invisible(compiled)
}

# Shared recompilation logic for both tcc_compile and tcc_link objects.
# Rebuilds the compiled code from the stored recipe and copies all
# bindings into the target environment.
recompile_into <- function(target) {
  link_args <- .subset2(target, ".link_args")
  if (!is.null(link_args)) {
    fresh <- do.call(tcc_link, link_args)
  } else {
    ffi <- .subset2(target, ".ffi")
    if (is.null(ffi)) {
      stop(
        "Cannot recompile: no FFI recipe stored in this object",
        call. = FALSE
      )
    }
    fresh <- tcc_compile(ffi)
  }
  for (nm in ls(fresh, all.names = TRUE)) {
    target[[nm]] <- fresh[[nm]]
  }
  target[[".valid"]] <- TRUE
  invisible(target)
}

# Helper for %||%
`%||%` <- function(x, y) if (is.null(x)) y else x

tcc_library_search_paths <- function(
  sysname = as.character(unname(Sys.info()[["sysname"]]))
) {
  platform_paths <- tcc_platform_lib_paths(sysname)

  env_vars <- unique(c(
    tcc_loader_env_key(sysname),
    "LIBRARY_PATH",
    if (identical(sysname, "Darwin")) {
      "DYLD_FALLBACK_LIBRARY_PATH"
    } else {
      character(0)
    }
  ))

  env_paths <- unlist(
    lapply(env_vars, function(key) {
      value <- Sys.getenv(key, unset = "")
      if (!nzchar(value)) {
        return(character(0))
      }
      strsplit(value, .Platform$path.sep, fixed = TRUE)[[1]]
    }),
    use.names = FALSE
  )

  unique(c(env_paths[nzchar(env_paths)], platform_paths))
}

# Search for library in platform-dependent paths
tcc_find_library <- function(name) {
  if (file.exists(name)) {
    return(name)
  }

  # Try platform-specific paths plus relevant loader/linker env paths.
  sysname <- as.character(unname(Sys.info()[["sysname"]]))
  paths <- tcc_library_search_paths(sysname)

  suffix_pattern <- c(
    Windows = "\\.dll$",
    Linux = "\\.so(\\..*)?$",
    Darwin = "\\.dylib(\\..*)?$"
  )[[sysname]]
  has_platform_suffix <- !is.null(suffix_pattern) &&
    grepl(suffix_pattern, name, ignore.case = identical(sysname, "Windows"))
  lib_name <- if (has_platform_suffix) {
    name
  } else {
    tcc_short_lib_filename(sysname, name)
  }

  for (path in paths) {
    if (dir.exists(path)) {
      full_path <- file.path(path, lib_name)
      if (file.exists(full_path)) {
        return(full_path)
      }
    }
  }

  # Not found
  NULL
}

#' Link an external shared library with Bun-style FFI bindings
#'
#' Link a system library (like libsqlite3) and generate type-safe
#' wrappers automatically using TinyCC JIT compilation (API mode).
#' Unlike dlopen(), this uses TinyCC to compile bindings that handle
#' type conversion between R and C automatically.
#'
#' @param path Library short name (e.g., `"m"`, `"sqlite3"`) or full path to
#'   the shared library. Short names stay on the normal linker-name path
#'   (`-l<name>`). File names such as `libm.so.6` or full paths are resolved
#'   through the configured library search paths when needed and linked as exact
#'   files rather than collapsed to generic names like `m`.
#' @param symbols Named list of symbol definitions with:
#'   \itemize{
#'     \item args: List of FFI types for arguments
#'     \item returns: FFI type for return value
#'   }
#' @param headers Optional C headers to include
#' @param libs Library names to link (e.g., "sqlite3")
#' @param lib_paths Additional library search paths
#' @param include_paths Additional include search paths
#' @param user_code Optional custom C code to include in the compilation
#' @param verbose Print debug information
#' @return A tcc_compiled object with callable functions
#' @export
#' @examples
#' \dontrun{
#' # Link SQLite with type-safe bindings
#' sqlite <- tcc_link(
#'   "libsqlite3.so",
#'   symbols = list(
#'     sqlite3_libversion = list(args = list(), returns = "cstring"),
#'     sqlite3_open = list(args = list("cstring", "ptr"), returns = "i32")
#'   ),
#'   libs = "sqlite3"
#' )
#'
#' # Call directly - type conversion happens automatically
#' sqlite$sqlite3_libversion()
#'
#' # Example with custom user code for helper functions
#' math_with_helpers <- tcc_link(
#'   "m",
#'   symbols = list(
#'     sqrt = list(args = list("f64"), returns = "f64"),
#'     safe_sqrt = list(args = list("f64"), returns = "f64")
#'   ),
#'   user_code = "
#'     #include <math.h>
#'
#'     // Helper function that validates input before calling sqrt
#'     double safe_sqrt(double x) {
#'       if (x < 0) {
#'         return NAN;
#'       }
#'       return sqrt(x);
#'     }
#'   ",
#'   libs = "m"
#' )
#' math_with_helpers$safe_sqrt(16.0)
#' math_with_helpers$safe_sqrt(-4.0) # Returns NaN for negative input
#' }
tcc_link <- function(
  path,
  symbols,
  headers = NULL,
  libs = character(0),
  lib_paths = character(0),
  include_paths = character(0),
  user_code = NULL,
  verbose = FALSE
) {
  # Find library if not absolute path. Keep short linker names such as
  # "m" or "sqlite3" on the -l<name> path. When the caller supplies a file
  # name or path, resolve it and link that exact file; versioned runtime files
  # such as libm.so.6 are not equivalent to the generic -lm linker name on all
  # systems.
  is_short_name <- !file.exists(path) &&
    !grepl("[/\\\\]", path) &&
    !grepl("\\.(so|dylib|dll)", path)

  if (!is_short_name && !file.exists(path) && !grepl("^/", path)) {
    found_path <- tcc_find_library(path)
    if (is.null(found_path)) {
      stop("Library not found: ", path, call. = FALSE)
    }
    path <- found_path
  }

  if (verbose) {
    message("Loading library: ", path)
    message("Symbols: ", paste(names(symbols), collapse = ", "))
  }

  # Create FFI context
  ffi <- tcc_ffi() |>
    tcc_output("memory")

  # Add library
  ffi$libraries <- libs
  ffi$lib_paths <- lib_paths
  ffi$include_paths <- include_paths

  # Process headers
  if (!is.null(headers)) {
    ffi$headers <- headers
  }

  # Process user code
  if (!is.null(user_code)) {
    ffi$c_code <- user_code
  }

  # Add the library path and link the library.
  if (!is_short_name) {
    ffi <- rtinycc_add_library_file(ffi, path)
  } else {
    # Short name like "m" — just pass to linker as -l<name>.
    ffi <- tcc_library(ffi, path)
  }

  # Store symbols as classed specs
  for (sym_name in names(symbols)) {
    ffi$symbols[[sym_name]] <- as_rtinycc_bound_symbol(
      sym_name,
      symbols[[sym_name]]
    )
  }

  # Mark as external library bindings
  attr(ffi, "external_lib") <- path
  attr(ffi, "is_external") <- TRUE

  # Generate C code with external declarations (always R API mode)
  c_code <- generate_ffi_code(
    symbols = ffi$symbols,
    headers = ffi$headers,
    c_code = ffi$c_code,
    is_external = TRUE,
    structs = ffi$structs,
    unions = ffi$unions,
    enums = ffi$enums,
    globals = ffi$globals,
    container_of = ffi$container_of,
    field_addr = ffi$field_addr,
    struct_raw_access = ffi$struct_raw_access,
    introspect = ffi$introspect
  )

  if (verbose) {
    message("Generated C code:\n", c_code)
  }

  state <- tcc_ffi_create_state(ffi, output = "memory")
  tcc_ffi_compile_state(
    state = state,
    c_code = c_code,
    libraries = ffi$libraries,
    target = paste0("FFI bindings for ", basename(path))
  )

  # Create compiled object
  compiled <- tcc_compiled_object(
    state,
    ffi$symbols,
    "memory",
    ffi$structs,
    ffi$unions,
    ffi$enums,
    ffi$globals,
    ffi$container_of,
    ffi$field_addr,
    ffi$struct_raw_access,
    ffi$introspect
  )

  # Store enough to re-link after deserialization
  compiled$.ffi <- ffi
  compiled$.link_args <- list(
    path = path,
    symbols = symbols,
    headers = headers,
    libs = libs,
    lib_paths = lib_paths,
    include_paths = include_paths,
    user_code = user_code
  )

  if (verbose) {
    message("Successfully loaded ", length(ffi$symbols), " symbols")
  }

  compiled
}

#' CString S3 Class
#'
#' Wrapper around a C string pointer with an optional cached R copy.
#' Ownership follows the underlying external pointer; this wrapper does not add
#' finalizer or freeing behavior on top of that pointer.
#'
#' @param ptr External pointer to C string
#' @param clone Whether to clone the string immediately (safe for R use)
#' @param owned Currently unused. Reserved for future finalizer support.
#' @return A tcc_cstring object
#' @export
tcc_cstring_object <- function(ptr, clone = TRUE, owned = FALSE) {
  if (!inherits(ptr, "externalptr")) {
    stop("Expected external pointer", call. = FALSE)
  }

  obj <- list(
    ptr = ptr,
    owned = owned,
    cached_string = NULL
  )

  if (clone) {
    # Immediately copy to R string
    addr <- get_external_ptr_addr(ptr)
    if (addr > 0) {
      # Read C string at address
      obj$cached_string <- tcc_read_cstring(ptr)
    }
  }

  # owned is currently reserved; the pointer itself may already carry
  # ownership/finalizer semantics (for example via tcc_cstring()).

  class(obj) <- "tcc_cstring"
  obj
}

#' Convert a `tcc_cstring` object to an R string
#'
#' @param x A `tcc_cstring` object.
#' @param ... Ignored.
#' @return A character scalar containing the string value. Returns the
#'   cached R copy when available; otherwise reads the current NUL-terminated
#'   C string from `x$ptr`.
#' @export
as.character.tcc_cstring <- function(x, ...) {
  if (!is.null(x$cached_string)) {
    return(x$cached_string)
  }
  tcc_read_cstring(x$ptr)
}

#' Print a `tcc_cstring` object
#'
#' @param x A `tcc_cstring` object.
#' @param ... Ignored.
#' @return The input `tcc_cstring` object, invisibly. Called for its side
#'   effect of printing the current string value.
#' @export
print.tcc_cstring <- function(x, ...) {
  str <- as.character(x)
  cat("<tcc_cstring> \"", str, "\"\n", sep = "")
  invisible(x)
}

# Helper to read C string from external pointer
read_c_string <- function(ptr) {
  tcc_read_cstring(ptr)
}

# ============================================================================
# Struct, Union, Enum Support
# ============================================================================

#' Declare struct for FFI helper generation
#'
#' Generate R-callable helpers for struct allocation, field access,
#' and pointer management. The struct must be defined in a header.
#'
#' @param ffi A tcc_ffi object
#' @param name Struct name (as defined in C header)
#' @param accessors Named list of field accessors where
#'   names are field names and values are FFI types (e.g., list(x="f64", y="f64")).
#'   Named nested struct fields can use `"struct:<name>"` to generate borrowed
#'   nested-view getters and copy-in setters (for example `child = "struct:child"`).
#' @return Updated tcc_ffi object
#' @export
#' @examples
#' \dontrun{
#' ffi <- tcc_ffi() |>
#'   tcc_header("#include <point.h>") |>
#'   tcc_struct("point", list(x = "f64", y = "f64", id = "i32"))
#' }
tcc_struct <- function(ffi, name, accessors) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  if (!is.character(name) || length(name) != 1) {
    stop("Struct name must be a single string", call. = FALSE)
  }

  if (!is.list(accessors) && !is.character(accessors)) {
    stop(
      "Accessors must be a named list or named character vector",
      call. = FALSE
    )
  }

  # Convert character vector to list if needed
  if (is.character(accessors)) {
    accessors <- as.list(accessors)
  }

  # Validate field types
  for (field_name in names(accessors)) {
    field_type <- accessors[[field_name]]
    # Handle complex field specs like list(type="cstring", size=20)
    if (is.list(field_type)) {
      type_name <- field_type$type %||% "ptr"
      if (isTRUE(field_type$array)) {
        if (is.null(field_type$size) || !is.numeric(field_type$size)) {
          stop(
            "Array field '",
            field_name,
            "' must include numeric 'size'",
            call. = FALSE
          )
        }
      }
    } else {
      type_name <- field_type
    }

    # Allow "struct:name" for nested structs
    if (!grepl("^struct:", type_name)) {
      check_ffi_type(
        type_name,
        paste0("struct '", name, "' field '", field_name, "'")
      )
    }
  }

  # Store struct definition
  if (is.null(ffi$structs)) {
    ffi$structs <- list()
  }
  ffi$structs[[name]] <- accessors

  ffi
}

#' Declare union for FFI helper generation
#'
#' Generate R-callable helpers for union allocation and member access.
#' The union must be defined in a header.
#'
#' @param ffi A tcc_ffi object
#' @param name Union name (as defined in C header)
#' @param members Named list of union members with FFI types
#' @param active Default active member for accessors
#' @return Updated tcc_ffi object
#' @export
#' @examples
#' \dontrun{
#' ffi <- tcc_ffi() |>
#'   tcc_union("data_variant",
#'     members = list(as_int = "i32", as_float = "f32"),
#'     active = "as_int"
#'   )
#' }
tcc_union <- function(ffi, name, members, active = NULL) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  # Validate member types
  for (mem_name in names(members)) {
    mem_type <- members[[mem_name]]
    if (is.list(mem_type)) {
      # Complex member like struct inside union
      if (!is.null(mem_type$type) && mem_type$type == "struct") {
        # Valid - nested struct
      } else {
        type_name <- mem_type$type %||% "ptr"
        check_ffi_type(
          type_name,
          paste0("union '", name, "' member '", mem_name, "'")
        )
      }
    } else {
      check_ffi_type(
        mem_type,
        paste0("union '", name, "' member '", mem_name, "'")
      )
    }
  }

  if (is.null(ffi$unions)) {
    ffi$unions <- list()
  }
  ffi$unions[[name]] <- list(members = members, active = active)

  ffi
}

#' Declare enum for FFI helper generation
#'
#' Generate R-callable helpers for enum constants and type conversions.
#' The enum must be defined in a header.
#'
#' @param ffi A tcc_ffi object
#' @param name Enum name (as defined in C header)
#' @param constants Character vector of constant names to export
#' @param export_constants Whether to export enum constants as R functions
#' @return Updated tcc_ffi object
#' @export
#' @examples
#' \dontrun{
#' ffi <- tcc_ffi() |>
#'   tcc_header("#include <errors.h>") |>
#'   tcc_enum("error_code", constants = c("OK", "ERROR"), export_constants = TRUE)
#' }
tcc_enum <- function(ffi, name, constants = NULL, export_constants = FALSE) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  if (is.null(ffi$enums)) {
    ffi$enums <- list()
  }
  ffi$enums[[name]] <- list(
    constants = constants,
    export_constants = export_constants || !is.null(constants)
  )

  ffi
}

#' Generate container_of helper for struct member
#'
#' Creates a function that recovers the parent struct pointer from
#' a pointer to one of its members. This is the classic Linux kernel
#' container_of macro made accessible from R.
#'
#' @param ffi A tcc_ffi object
#' @param struct_name Struct name
#' @param member_name Member field name to compute offset from
#' @return Updated tcc_ffi object
#' @export
#' @examples
#' \dontrun{
#' ffi <- tcc_ffi() |>
#'   tcc_struct("student", list(id = "i32", marks = "i32")) |>
#'   tcc_container_of("student", "marks") # Creates struct_student_from_marks()
#' }
tcc_container_of <- function(ffi, struct_name, member_name) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  field_spec <- rtinycc_struct_field_spec(ffi, struct_name, member_name)
  if (rtinycc_is_bitfield_spec(field_spec)) {
    stop("container_of does not support bitfield members", call. = FALSE)
  }

  if (is.null(ffi$container_of)) {
    ffi$container_of <- list()
  }
  ffi$container_of[[struct_name]] <- c(
    ffi$container_of[[struct_name]],
    member_name
  )

  ffi
}

#' Generate field address getter helpers
#'
#' Creates functions that return pointers to specific struct fields.
#' Useful for passing field pointers to C functions or for container_of.
#'
#' @param ffi A tcc_ffi object
#' @param struct_name Struct name
#' @param fields Character vector of field names
#' @return Updated tcc_ffi object
#' @export
#' @examples
#' \dontrun{
#' ffi <- tcc_ffi() |>
#'   tcc_struct("point", list(x = "f64", y = "f64")) |>
#'   tcc_field_addr("point", c("x", "y")) # point_x_addr(), point_y_addr()
#' }
tcc_field_addr <- function(ffi, struct_name, fields) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  for (field_name in fields) {
    field_spec <- rtinycc_struct_field_spec(ffi, struct_name, field_name)
    if (rtinycc_is_bitfield_spec(field_spec)) {
      stop("field_addr does not support bitfield members", call. = FALSE)
    }
  }

  if (is.null(ffi$field_addr)) {
    ffi$field_addr <- list()
  }
  ffi$field_addr[[struct_name]] <- c(
    ffi$field_addr[[struct_name]],
    fields
  )

  ffi
}

#' Enable raw byte access for struct
#'
#' Generates helper functions to read/write raw bytes from struct memory.
#' Useful for bitwise operations, debugging, or manual serialization.
#'
#' @param ffi A tcc_ffi object
#' @param struct_name Struct name
#' @return Updated tcc_ffi object
#' @export
tcc_struct_raw_access <- function(ffi, struct_name) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  if (is.null(ffi$struct_raw_access)) {
    ffi$struct_raw_access <- character()
  }
  ffi$struct_raw_access <- c(ffi$struct_raw_access, struct_name)

  ffi
}

#' Enable introspection helpers
#'
#' Generates sizeof, alignof, and offsetof helper functions for
#' structs, unions, and enums. Useful for debugging or when you need
#' to know C layout information from R.
#'
#' @param ffi A tcc_ffi object
#' @return Updated tcc_ffi object
#' @export
tcc_introspect <- function(ffi) {
  if (!inherits(ffi, "tcc_ffi")) {
    stop("Expected tcc_ffi object", call. = FALSE)
  }

  ffi$introspect <- TRUE
  ffi
}

Try the Rtinycc package in your browser

Any scripts or data that you put into this service are public.

Rtinycc documentation built on April 28, 2026, 1:07 a.m.