V bindings to QuickJS javascript engine. Run JS in V.
The first version of this project was derived from
herudi/vjs. Thanks to the original author for
the foundational work that helped kick off vjsx.
- Evaluate js (code, file, module, etc).
- Multi evaluate support.
- Callback function support.
- Set-Globals support.
- Set-Module support.
- Call V from JS.
- Call JS from V.
- Top-Level
awaitsupport. usingvjsx.type_module.
v install vjsxIf you already have a local QuickJS checkout, you can compile vjsx against the
source tree directly instead of the bundled prebuilt archives.
This is useful when:
- you are on an unsupported architecture such as
macOS arm64 - you want to use a newer QuickJS version
- you do not want to maintain extra prebuilt
.afiles inside this repo
Example:
VJS_QUICKJS_PATH=/Users/guweigang/Source/quickjs \
v -d build_quickjs run main.vNotes:
VJS_QUICKJS_PATHshould point to the QuickJS source root that containsquickjs.c,quickjs-libc.c,quickjs.h, andquickjs-libc.h.- In this mode
vjsxcompiles QuickJS C sources directly. - Without
-d build_quickjs,vjsxuses the bundled headers underlibs/include/together with the prebuilt archives inlibs/.
Create file main.v and copy-paste this code.
import vjsx
fn main() {
mut session := vjsx.new_runtime_session()
defer {
session.close()
}
ctx := session.context()
value := ctx.eval('1 + 2') or { panic(err) }
ctx.end()
defer {
value.free()
}
assert value.is_number() == true
assert value.is_string() == false
assert value.to_int() == 3
println(value)
// 3
}If you are embedding vjsx into another V project, start from
runtimejs.ExtensionSession rather than lower-level runtime plumbing.
import runtimejs
import vjsx
fn host_api() vjsx.HostValueBuilder {
return vjsx.host_object(vjsx.HostObjectField{
name: 'app'
value: vjsx.host_object(vjsx.HostObjectField{
name: 'name'
value: vjsx.host_value('demo-host')
})
})
}
fn main() {
mut extension_session := runtimejs.new_node_extension_session(
vjsx.ContextConfig{},
vjsx.NodeRuntimeConfig{
process_args: ['extension.mjs']
},
vjsx.HostApiConfig{},
host_api(),
)
defer {
extension_session.close()
}
mut extension := extension_session.load_extension('./examples/js/host_extension.mjs',
vjsx.ScriptPluginHooks{}) or { panic(err) }
defer {
extension.close()
}
result := extension.call_export('greet', 'world') or { panic(err) }
defer {
result.free()
}
println(result.to_string())
}For the full host-first embedding guidance, see EMBEDDING.md.
v run main.vWith a local QuickJS checkout:
VJS_QUICKJS_PATH=/Users/guweigang/Source/quickjs \
v -d build_quickjs run main.vExplore examples
If you want the smallest file-based example, see
examples/run_file.v together with examples/js/foo.js.
If you want the recommended embedded-host flow, see
examples/embedding_extension.v together with
examples/js/host_extension.mjs.
If you also want an example that shows host modules plus manifest-defined hook
names, see examples/embedding_extension_manifest.v together with
examples/js/host_extension_manifest.mjs.
You can also run JS files directly from the repository:
./vjsx ./tests/test.jsModule mode:
./vjsx --module ./examples/js/main.jsTypeScript entry files are also supported:
./vjsx ./tests/ts_basic.ts
./vjsx --module ./tests/ts_module_runtime.mtsTypeScript module graphs are also supported, including:
- relative
.ts/.mtsimports - nearest
tsconfig.json, includingextends compilerOptions.baseUrlandpaths- bare package imports resolved from local
node_modules - package
exportsroot and explicit subpath entries
Options:
--module,-m: run the file as an ES module
This is runtime transpilation backed by the bundled typescript.js, and the
same loader is now also available from the vjsx API through
ctx.install_typescript_runtime() and ctx.run_runtime_entry(...).
It is a good fit for standalone .ts scripts, .mts modules, and small local
module graphs. Project-wide features like full tsc diagnostics, references,
and broader Node compatibility are still out of scope for now.
When embedding vjsx in a long-lived process, always pair each created
Runtime/Context with an explicit free(). Repeated TypeScript bootstrap
work in the same process assumes those runtimes are torn down deliberately;
leaking them can surface later as hard-to-diagnose bootstrap failures.
If you want one owner object for embedded use, prefer vjsx.new_runtime_session()
and session.close(), which tear down the Context and Runtime together.
For Node-style hosts, that teardown also closes tracked sqlite / mysql
connections that were left open by JS code.
If you also want TypeScript/module-aware file loading from the same session,
use runtimejs.new_script_runtime_session(...) or
runtimejs.new_node_runtime_session(...). Those session helpers install the
runtime bridge so embedders can call higher-level methods like:
session.run(path)session.run_script(path)session.run_module(path)session.load_module(path)session.import_module(path)session.import_module_with_host(path, host_api)session.load_plugin(path, hooks)session.load_plugin_with_host(path, hooks, host_api)session.call_module_export(path, export_name, ...)session.call_module_export_with_host(path, export_name, host_api, ...)session.call_module_method(path, export_name, method_name, ...)session.call_module_method_with_host(path, export_name, method_name, host_api, ...)session.call_default_export_method(path, method_name, ...)session.call_default_export_method_with_host(path, method_name, host_api, ...)
For embedded host use, the recommended abstraction ladder is now:
vjsx.RuntimeSession: core lifecycle and loadingruntimejs.ExtensionSession: default embedder-facing sessionruntimejs.ExtensionHandle: one loaded extension instance with lifecycle hooks plus regular export calls
That path is documented in EMBEDDING.md, together with:
- the recommended stopping point to avoid over-design
- API surface guidance for default vs advanced helpers
- stability notes for likely long-term vs de-emphasized APIs
- a convergence checklist for future cleanup without more abstraction growth
- host API shape guidance
load_extension(...)usage- optional JS/TS manifest support
- optional manifest
servicessupport
vjsx.new_runtime() and rt.new_context() are still available for advanced
manual ownership cases, but then the caller is responsible for pairing them
with ctx.free() and rt.free() correctly.
The wrapper script will use VJS_QUICKJS_PATH when it is set. If it is not
set, it will try ../quickjs relative to the repository root as a local
convenience fallback.
Currently support linux/mac/win (x64).
in windows, requires
-cc gcc.
The runtime is now split into clearer layers:
ctx.install_runtime_globals(...): reusable globals likeBuffer, timers,URL, andURLPatternctx.install_node_compat(...): Node-like host features such asconsole,fs,path,os,child_process,process, standardfetchglobals,sqlite, and optionalmysqlweb.inject_browser_host(ctx, ...): browser-style host features underweb/, includingwindow, DOM bootstrap, and Web APIs
web.inject_browser_host(...) is now configurable, so you can expose only the
browser-facing modules you want, while still letting higher-level features like
fetch pull in their required Web API dependencies.
The legacy ctx.install_host(...) entrypoint still works as a compatibility
wrapper around install_node_compat(...).
For the embedding ownership, event-loop, timer, diagnostics, limits, and profile
contracts, see docs/RUNTIME_CONTRACT.md.
For embedders, ctx.install_host_api(...) provides a more explicit way to
expose host globals and modules to JS/TS extension code without hand-rolling
js_module(...).create() at every call site.
Useful embedders helpers include:
vjsx.host_value(...)vjsx.host_object(...)vjsx.host_module_exports(...)vjsx.host_module_object(...)
Database host modules:
import { open } from "sqlite"is available in the default Node-style host profileimport { connect } from "mysql"is also exposed, but the real V MySQL backend is only compiled when you pass-d vjsx_mysql- The CLI forwards extra V compiler flags through
VJS_V_FLAGS, for example:VJS_V_FLAGS='-d vjsx_mysql' ./vjsx --module app.mjs - End-to-end example files live under
examples/db/
SQLite example:
import { open } from "sqlite";
const db = await open({ path: "./app.db", busyTimeout: 1000 });
await db.exec("create table if not exists users (id integer primary key, name text)");
await db.execMany("insert into users(name) values (?)", [["alice"], ["bob"]]);
const firstUser = await db.queryOne("select id, name from users order by id");
const userCount = await db.scalar("select count(*) from users");
console.log(firstUser ? firstUser.name : "null", userCount);
await db.close();MySQL example:
import { connect } from "mysql";
const db = await connect({
host: "127.0.0.1",
port: 3306,
user: "root",
password: "",
database: "mysql",
});
const stmt = await db.prepareCached("select id, name from users where name <> ? order by id");
const rows = await stmt.query(["carol"]);
console.log(rows.length);
await stmt.close();
await db.close();DB host API shape:
sqlite.open({ path, busyTimeout? })mysql.connect({ host?, port?, user?|username?, password?, database?|dbname? })db.query(sql, params?)db.queryOne(sql, params?)db.scalar(sql, params?)db.queryMany(sql, [[...], [...]])db.exec(sql, params?)db.execMany(sql, [[...], [...]])await db.prepareCached(sql)reuses the same prepared statement for repeated SQL text until that statement is closedstmt.close()anddb.close()are idempotent, anddb.close()also marks cached/reusable statements as closeddb.begin()db.commit()db.rollback()db.transaction(async (tx) => { ... })await db.prepare(sql)returning a reusable statement withquery(params?),queryOne(params?),scalar(params?),queryMany([[...], [...]]),exec(params?),execMany([[...], [...]]), andclose()db.close()mysqlconnections also exposedb.ping()db.driveridentifies the backend, for examplesqliteormysqldb.supportsTransactionstells you whether transaction helpers are availabledb.inTransactionreflects the host connection's current transaction statedb.toString()andstmt.toString()provide compact debug-friendly summariesdb.exec(...)returnsrows,changes,rowsAffected,lastInsertRowid, andinsertId
process.env is exposed as a live host view, so reads reflect environment
variable changes made by the embedding process after the runtime was installed.
- statements expose
driver,supportsTransactions,sql,kind, andclosed
When params are provided to mysql.query(...) or mysql.exec(...), vjsx
now routes them through V's prepared statement support instead of expanding SQL
placeholders in user space.
For lifecycle-sensitive code, cached statements are scoped to the connection:
prepareCached(...) returns the same statement for repeated SQL text until that
statement is closed, and db.close() marks all cached/reusable statements as
closed.
For local or CI integration tests against a live MySQL server, the optional
tests/host_mysql_runtime_test.v probe reads VJS_TEST_MYSQL_HOST,
VJS_TEST_MYSQL_PORT, VJS_TEST_MYSQL_USER, VJS_TEST_MYSQL_PASSWORD,
VJS_TEST_MYSQL_DBNAME, and VJS_TEST_MYSQL_TABLE.
Useful presets:
vjsx.runtime_globals_full()vjsx.runtime_globals_minimal()vjsx.node_compat_full(fs_roots, process_args)vjsx.node_compat_minimal(fs_roots, process_args)web.browser_host_full()web.browser_host_minimal()
Higher-level runtime entrypoints:
ctx.install_script_runtime(...)ctx.install_node_runtime(...)web.inject_browser_runtime(ctx)web.inject_browser_runtime_minimal(ctx)
CLI runtime profiles:
./vjsx --runtime node ..../vjsx --runtime script ..../vjsx --runtime browser --module ...
The CLI defaults to --runtime node for backwards compatibility.
browser is intentionally a pure browser-style host profile and currently
requires --module. The current CLI browser profile exposes browser-like
globals such as window, self, EventTarget, URL, timers, streams,
Blob, and FormData, while intentionally leaving out Node globals like
process, Buffer, and modules such as fs.
Example:
import vjsx
import herudi.vjsx.web
fn main() {
mut session := vjsx.new_script_runtime_session(vjsx.ContextConfig{}, vjsx.ScriptRuntimeConfig{
process_args: ['inline.js']
})
defer {
session.close()
}
ctx := session.context()
web.inject_browser_runtime_minimal(ctx)
}ctx.eval('const sum = (a, b) => a + b') or { panic(err) }
ctx.eval('const mul = (a, b) => a * b') or { panic(err) }
sum := ctx.eval('sum(${1}, ${2})') or { panic(err) }
mul := ctx.eval('mul(${1}, ${2})') or { panic(err) }
ctx.end()
println(sum)
// 3
println(mul)
// 2glob := ctx.js_global()
glob.set('foo', 'bar')
value := ctx.eval('foo') or { panic(err) }
ctx.end()
println(value)
// barmut mod := ctx.js_module('my-module')
mod.export('foo', 'foo')
mod.export('bar', 'bar')
mod.export_default(mod.to_object())
mod.create()
code := '
import mod, { foo, bar } from "my-module";
console.log(foo, bar);
console.log(mod);
'
ctx.eval(code, vjsx.type_module) or { panic(err) }
ctx.end()import vjsx
mut session := vjsx.new_runtime_session()
defer {
session.close()
}
ctx := session.context()
ctx.install_host_api(
globals: [
vjsx.HostGlobalBinding{
name: 'appName'
value: vjsx.host_value('demo')
},
]
modules: [
vjsx.HostModuleBinding{
name: 'host-tools'
install: vjsx.host_module_exports(
vjsx.HostModuleExport{
name: 'answer'
value: vjsx.host_value(42)
},
vjsx.HostModuleExport{
name: 'describe'
value: fn [ctx] (ctx2 &vjsx.Context) vjsx.Value {
return ctx.js_function(fn [ctx] (args []vjsx.Value) vjsx.Value {
return ctx.js_string('host:' + args[0].str())
})
}
},
)
},
]
)
ctx.eval('
import hostTools, { answer, describe } from "host-tools";
globalThis.result = [
appName,
String(answer),
describe("ok"),
String(hostTools.answer)
].join("|");
', vjsx.type_module) or { panic(err) }ctx.install_host_api(
globals: [
vjsx.HostGlobalBinding{
name: 'host'
value: vjsx.host_object(
vjsx.HostObjectField{
name: 'name'
value: vjsx.host_value('demo')
},
vjsx.HostObjectField{
name: 'math'
value: vjsx.host_object(
vjsx.HostObjectField{
name: 'add'
value: fn [ctx] (ctx2 &vjsx.Context) vjsx.Value {
return ctx.js_function(fn [ctx] (args []vjsx.Value) vjsx.Value {
return ctx.js_int(args[0].to_int() + args[1].to_int())
})
}
},
)
},
)
},
]
modules: [
vjsx.HostModuleBinding{
name: 'host-service'
install: vjsx.host_module_object(
vjsx.HostObjectField{
name: 'version'
value: vjsx.host_value('v1')
},
vjsx.HostObjectField{
name: 'greet'
value: fn [ctx] (ctx2 &vjsx.Context) vjsx.Value {
return ctx.js_function(fn [ctx] (args []vjsx.Value) vjsx.Value {
return ctx.js_string('hello:' + args[0].str())
})
}
},
)
},
]
)Inject Web API to vjsx.
import vjsx
import herudi.vjsx.web
fn main() {
mut session := vjsx.new_runtime_session()
defer {
session.close()
}
ctx := session.context()
// inject all browser host features
web.inject_browser_host(ctx)
// or inject one by one
// web.console_api(ctx)
// web.encoding_api(ctx)
// more..
...
}- Console
- setTimeout, clearTimeout
- setInterval, clearInterval
- btoa, atob
- URL
- URLSearchParams
- URLPattern
- Encoding API
- Crypto API
- SubtleCrypto
- digest
-
CryptoKey -
generateKey()forHMAC,Ed25519,ECDSA,AES-CBC, andAES-CTR -
importKey('raw')forHMAC,PBKDF2,AES-CBC,AES-CTR, andEd25519public keys -
exportKey('raw')for extractableHMAC/AESkeys, and generatedEd25519/ECDSApublic keys -
deriveBits()forPBKDF2(SHA-256/384/512) -
deriveKey()forPBKDF2->HMAC/AES-CBC/AES-CTR - encrypt (
AES-CBC,AES-CTRwithlength = 128) - decrypt (
AES-CBC,AES-CTRwithlength = 128) - sign (
HMAC,Ed25519,ECDSA) - verify (
HMAC,Ed25519,ECDSA)
Current SubtleCrypto scope:
| Area | Current support |
|---|---|
digest |
SHA-1, SHA-256, SHA-384, SHA-512 |
HMAC |
generateKey, importKey('raw'), exportKey('raw'), sign, verify |
AES-CBC |
generateKey, importKey('raw'), exportKey('raw'), encrypt, decrypt |
AES-CTR |
generateKey, importKey('raw'), exportKey('raw'), encrypt, decrypt with length = 128 only |
PBKDF2 |
importKey('raw'), deriveBits, deriveKey with SHA-256, SHA-384, SHA-512 |
Ed25519 |
generateKey, sign, verify, importKey('raw') for public keys, exportKey('raw') for generated public keys |
ECDSA |
generateKey, sign, verify, exportKey('raw') for generated public keys |
Notes:
AES-GCMis not implemented yet.ECDSAcurrently supports generated key pairs only; fullimportKey()/structured export formats are not implemented yet.Ed25519andECDSAsupport inexportKey('raw')is intentionally limited to public keys.PBKDF2is a base-key flow only; useimportKey('raw', ...)beforederiveBits()orderiveKey().
Minimal examples:
These snippets assume you are running with the browser-style host profile, so
crypto.subtle and TextEncoder are already available.
Runnable copies of these snippets live under examples/webcrypto/ and can be run
with:
./vjsx --runtime browser --module ./examples/webcrypto/<file>.mjsSee also: examples/webcrypto/README.md
HMAC sign/verify:
File: examples/webcrypto/hmac_sign_verify.mjs
const text = new TextEncoder().encode("hello");
const key = await crypto.subtle.importKey(
"raw",
new Uint8Array([1, 2, 3, 4]),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"],
);
const sig = await crypto.subtle.sign("HMAC", key, text);
const ok = await crypto.subtle.verify("HMAC", key, sig, text);
console.log(sig.byteLength, ok);AES-CBC encrypt/decrypt:
File: examples/webcrypto/aes_cbc_encrypt_decrypt.mjs
const text = new TextEncoder().encode("hello");
const iv = new Uint8Array([15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
const key = await crypto.subtle.importKey(
"raw",
new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
"AES-CBC",
true,
["encrypt", "decrypt"],
);
const encrypted = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, key, text);
const decrypted = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, encrypted);
console.log(encrypted.byteLength, new TextDecoder().decode(decrypted));PBKDF2 derive an AES key:
File: examples/webcrypto/pbkdf2_derive_aes.mjs
const password = new TextEncoder().encode("password");
const baseKey = await crypto.subtle.importKey(
"raw",
password,
"PBKDF2",
false,
["deriveBits", "deriveKey"],
);
const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: new TextEncoder().encode("salt"),
iterations: 1000,
hash: "SHA-256",
},
baseKey,
{ name: "AES-CBC", length: 128 },
true,
["encrypt", "decrypt"],
);
console.log(aesKey.algorithm.name, aesKey.algorithm.length);Ed25519 and ECDSA:
File: examples/webcrypto/signatures.mjs
const text = new TextEncoder().encode("hello");
const ed = await crypto.subtle.generateKey("Ed25519", false, ["sign", "verify"]);
const edSig = await crypto.subtle.sign("Ed25519", ed.privateKey, text);
console.log(await crypto.subtle.verify("Ed25519", ed.publicKey, edSig, text));
const ec = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
false,
["sign", "verify"],
);
const ecSig = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
ec.privateKey,
text,
);
console.log(await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
ec.publicKey,
ecSig,
text,
));