R Internal Functions: .Internal(), .Call(), .External() — Low Level
Many R functions are thin wrappers around compiled C code. .Primitive() and .Internal() call built-in C routines inside R itself, while .Call() and .External() call C code in add-on packages. Understanding this layer explains why some functions are fast and why you can't always see their source code.
.Primitive() — The Fastest R Functions
Primitive functions are implemented directly in C with no R-level wrapper. They're the fastest operations in R:
# These are all primitive functions
cat("typeof(sum):", typeof(sum), "\n")
cat("typeof(`+`):", typeof(`+`), "\n")
cat("typeof(`[`):", typeof(`[`), "\n")
cat("typeof(c):", typeof(c), "\n")
# Printing a primitive shows its C entry point
print(sum)
print(`+`)
# You can identify primitives by their type
base_fns <- ls("package:base")
primitives <- Filter(function(fn) is.primitive(get(fn, "package:base")), base_fns)
cat("Total base functions:", length(base_fns), "\n")
cat("Primitive functions:", length(primitives), "\n")
cat("\nSample primitives:", paste(head(primitives, 10), collapse = ", "), "\n")
.Internal() — Built-in R Functions
.Internal() calls C code that's compiled into R itself, but through a regular R function wrapper:
# Many base functions use .Internal internally
# For example, paste() is an R function that calls .Internal(paste(...))
# You can see this by looking at the source
cat("body of grep:\n")
print(body(grep))
# The difference: .Primitive skips R overhead, .Internal goes through R dispatch
cat("\nsum is primitive (no R wrapper):\n")
print(sum)
cat("\ngamma is a regular function using .Internal:\n")
# We can see the structure of functions that call .Internal
is_internal <- function(fn_name) {
fn <- get(fn_name, "package:base")
if (is.primitive(fn)) return("primitive")
b <- body(fn)
if (is.null(b)) return("primitive")
if (any(grepl("\\.Internal", deparse(b)))) return(".Internal")
"R-level"
}
samples <- c("sum", "paste", "grep", "mean", "which", "length")
for (fn in samples) {
cat(sprintf(" %s: %s\n", fn, is_internal(fn)))
}
.Call() and .External() — Package C Code
.Call() is the modern interface for R packages to call their own compiled C/C++ code:
# Many CRAN packages use .Call for performance-critical code
# For example, stringr/stringi use C++ for string operations
# You can see .Call in package source code:
# function(x) {
# .Call("C_my_fast_function", x, PACKAGE = "mypackage")
# }
# Demonstrate: some base R functions also use .Call
cat("Functions in base that use compiled code are common.\n")
cat("The pattern is: R function validates input, then calls C for speed.\n\n")
# Example: nchar uses C internally for speed
x <- rep("hello world", 100000)
t <- system.time(result <- nchar(x))
cat("nchar on 100k strings:", t["elapsed"], "sec\n")
cat("Result sample:", result[1], "characters\n")
Summary of Interfaces
Interface
Where the C code lives
Who uses it
Speed
.Primitive()
R core (no R wrapper)
Base R only
Fastest
.Internal()
R core (R wrapper)
Base R only
Very fast
.Call()
Package shared library
Any package
Fast
.External()
Package shared library
Older packages
Fast
.C()
Package shared library
Legacy interface
Fast (limited types)
# Quick way to see if a function hits C code
check_function <- function(fn_name, pkg = "base") {
fn <- get(fn_name, paste0("package:", pkg))
if (is.primitive(fn)) {
cat(fn_name, "is primitive (direct C)\n")
} else {
src <- deparse(body(fn))
if (any(grepl("\\.Internal", src))) cat(fn_name, "uses .Internal\n")
else if (any(grepl("\\.Call", src))) cat(fn_name, "uses .Call\n")
else cat(fn_name, "is pure R\n")
}
}
check_function("sum")
check_function("paste")
check_function("mean")
check_function("which")
FAQ
Can I call .Internal() myself?
No. .Internal() can only be called from base R functions. If you try eval(.Internal(paste(...))) from user code, R blocks it for security and stability. Only .Call() is available for package authors.
Why does print(sum) show ".Primitive" instead of source code?
Primitive functions have no R source code — they're implemented entirely in C. The R interpreter recognizes the function name and jumps directly to compiled code without creating an execution environment.
What's Next?
R Promise Objects — lazy evaluation and the force() function
R Namespaces — how packages organize their exported and internal functions
R Environments — the environment system that all function calls rely on