How Deno's Runtime Extension System Works: A Deep Dive into deno_core
Deno's runtime extension system uses the Extension struct in deno_core to bundle Rust ops and JavaScript source, registering them with V8 during JsRuntime initialization to create a modular, permission-aware bridge between native code and JavaScript.
The denoland/deno repository implements its JavaScript/TypeScript runtime through a layered architecture where native Rust capabilities are exposed to JavaScript via a structured extension system. This design allows Deno to ship core APIs as discrete, optional modules while maintaining high performance through V8 snapshotting.
Core Architecture and Types
The extension system centers on four key types defined across the deno_core and runtime crates.
deno_core::Extension — Defined in [deno_core/src/ext.rs](https://github.com/denoland/deno/blob/main/deno_core/src/ext.rs), this struct holds op registrations, JavaScript/ESM source files, and metadata required to bridge Rust and V8.
deno_core::ExtensionFileSource — Located in the same file, this type represents JavaScript source that can be inline, static, or computed dynamically.
runtime::worker::JsRuntime — Found in [runtime/worker.rs](https://github.com/denoland/deno/blob/main/runtime/worker.rs), this core runtime accepts a Vec<Extension> during instantiation and manages the V8 isolate lifecycle.
runtime::snapshot::Snapshot — Implemented in [runtime/snapshot.rs](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs), this type pre-creates a V8 snapshot containing core extensions to eliminate cold-start latency.
The Extension Registration Flow
Deno registers native capabilities through a five-phase pipeline that transforms Rust functions into callable JavaScript APIs.
Defining Ops with the #[op] Macro
Operations (ops) are Rust functions decorated with the #[op] procedural macro. Each op becomes a V8 function accessible from JavaScript.
// ext/web/lib.rs
use deno_core::op;
#[op]
async fn op_fetch(req: FetchRequest) -> Result<FetchResponse, AnyError> {
// Native implementation...
}
The macro automatically generates the V8 binding code required to marshal data between JavaScript and Rust.
Building Extensions with Extension::builder()
Each extension crate exposes an initialization function that constructs an Extension using the builder pattern. This bundles ops with their associated JavaScript runtimes.
// ext/web/lib.rs
pub fn init_ops_and_esm() -> Extension {
Extension::builder()
.ops(vec![
("op_fetch", op_fetch::decl()),
// Additional ops...
])
.esm_files(vec![
ExtensionFileSource::js_script(
"ext:deno_web/99_main.js",
include_str!("js/99_main.js"),
),
])
.build()
}
The Extension::builder() API lives in deno_core::Extension and supports both ESM files and traditional scripts.
Aggregating Extensions in the Runtime
The Deno binary collects all core extensions into a vector before creating the runtime. In [runtime/snapshot.rs](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs), the system aggregates standard library extensions:
// runtime/snapshot.rs
let mut extensions: Vec<Extension> = vec![
deno_web::init_ops_and_esm(),
deno_net::init_ops_and_esm(),
deno_fs::init_ops_and_esm(),
// ...other core extensions
];
This vector is then passed to JsRuntime::new in [runtime/worker.rs](https://github.com/denoland/deno/blob/main/runtime/worker.rs):
let js_runtime = JsRuntime::new(RuntimeOptions {
extensions,
// Additional options...
});
At this point, V8 registers each op and compiles the provided JavaScript source into the isolate.
Customizing Extensions Before Runtime Creation
Some consumers require dynamic modification of extensions before runtime initialization. The CLI's TypeScript compiler in [cli/tsc/js.rs](https://github.com/denoland/deno/blob/main/cli/tsc/js.rs) demonstrates this pattern using a customizer closure:
let mut ext = deno_cli_tsc::init_ops_and_esm();
let customizer = |ext: &mut Extension| {
use deno_core::ExtensionFileSource;
ext.esm_files.to_mut().push(
ExtensionFileSource::new_computed(
"ext:deno_cli_tsc/99_main_compiler.js",
crate::tsc::MAIN_COMPILER_SOURCE.as_str().into(),
),
);
};
customizer(&mut ext);
This technique allows injection of context-specific scripts without modifying the base extension crate.
V8 Snapshotting for Performance
To eliminate startup overhead, Deno bakes core extensions into a V8 snapshot during the build process. The [runtime/snapshot.rs](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs) module creates this snapshot by calling get_extensions_in_snapshot from [runtime/snapshot_info.rs](https://github.com/denoland/deno/blob/main/runtime/snapshot_info.rs):
// runtime/snapshot.rs (simplified)
let mut extensions = get_extensions_in_snapshot();
let mut snapshot = Snapshot::new(&mut extensions, include_js_files);
When JsRuntime is instantiated with snapshot: Some(snapshot_blob), V8 restores the pre-initialized isolate. This bypasses the cost of re-registering ops and re-parsing core JavaScript modules on every process start.
Creating a Custom Extension
Third-party developers and embedders can create custom extensions using the same primitives as Deno's core APIs.
use deno_core::{Extension, op, JsRuntime, RuntimeOptions, ExtensionFileSource};
// 1. Define the native operation.
#[op]
fn op_hello(name: String) -> String {
format!("Hello, {name}!")
}
// 2. Construct the extension.
fn hello_extension() -> Extension {
Extension::builder()
.ops(vec![("op_hello", op_hello::decl())])
.esm_files(vec![
ExtensionFileSource::js_script(
"ext:my_hello/hello.js",
r#"
export function hello(name) {
return Deno.core.ops.op_hello(name);
}
"#,
),
])
.build()
}
// 3. Initialize the runtime with the custom extension.
fn main() {
let extensions = vec![hello_extension()];
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions,
..Default::default()
});
// 4. Execute JavaScript utilizing the new API.
runtime
.execute_script(
"<anon>",
r#"
import { hello } from "ext:my_hello/hello.js";
console.log(hello("Deno"));
"#,
)
.unwrap();
}
This pipeline demonstrates the complete lifecycle: Rust op definition, extension construction, runtime injection, and JavaScript consumption via the Deno.core.ops namespace.
Summary
deno_core::Extensionindeno_core/src/ext.rsserves as the fundamental abstraction for bundling Rust ops and JavaScript source.- The
#[op]macro transforms Rust functions into V8-callable operations, whileExtension::builder()assembles them into deployable units. - Registration occurs when
JsRuntime::newreceives aVec<Extension>, at which point ops are bound to the V8 isolate. - Snapshotting in
runtime/snapshot.rspre-compiles core extensions into V8 snapshots, reducing cold-start latency by avoiding re-registration and re-parsing. - Customization closures allow dynamic script injection before runtime initialization, as seen in the TypeScript compiler integration.
- Extensions live in dedicated crates (e.g.,
ext/web,ext/net,ext/fs), enabling modular API development and permission isolation.
Frequently Asked Questions
What is the difference between deno_core::Extension and JsRuntime?
deno_core::Extension is a static definition that bundles Rust ops and JavaScript source code, essentially a blueprint for capabilities. JsRuntime, defined in runtime/worker.rs, is the active V8 isolate that consumes extensions during instantiation. While an Extension describes what APIs are available, JsRuntime creates the execution environment where those APIs become callable JavaScript functions.
How does Deno's extension system improve startup performance?
Deno leverages V8 snapshots to eliminate initialization overhead. During the build process, runtime/snapshot.rs creates a snapshot containing pre-registered core extensions from runtime/snapshot_info.rs. When a new process starts, JsRuntime restores this snapshot rather than re-registering ops and re-parsing JavaScript, reducing cold-start time from hundreds of milliseconds to near-instant.
Can I remove core extensions to create a minimal Deno runtime?
Yes, because Deno organizes each API domain into separate extension crates (ext/web, ext/net, ext/crypto), you can construct a custom Vec<Extension> containing only required modules. When creating JsRuntime in runtime/worker.rs, simply omit unnecessary extensions from the vector. This is particularly useful for embedded scenarios or security-constrained environments where network or filesystem access must be prohibited at the runtime level.
How do permissions integrate with the extension system?
Permissions are enforced at the op layer. When an extension registers an op like op_fetch in ext/fetch/lib.rs or op_read_file in ext/fs, the Rust implementation checks permission states before executing privileged operations. Because extensions are modular, Deno can enable or disable entire capability domains by including or excluding the respective extension, creating a coarse-grained permission boundary complemented by fine-grained runtime checks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →