- Foreword: Target Platforms
- The Canonical Builds
- C Files
- JS Files
- Building the JS/WASM Files
- Exporting New APIs to JS
This page introduces the various builds of the WASM/JS code and how to build customized versions.
sqlite3.js
and sqlite3.wasm
are the "standard builds." They are
built for wide deployment on a number of browsers.
The GNUmakefile in the ext/wasm
directory of the source
tree creates those files. The rest of this document describes what
goes in to building them, so that clients may create custom builds and
adapt the builds for toolchains other than Emscripten.
These instructions assume:
- A Linux-like system which supports...
- A recent version of the Emscripten SDK. This project can be built with other WASM toolchains but currently cannot do much when built without the Emscripten-provided file I/O emulation layer. Improving support for other build environments is an ongoing project goal.
Foreword: Target Platforms
This project's primary deliverable is "lowest common denominator" JavaScript which can be used in a wide variety of environments, with web browsers taking priority over server-side JS engines. We do not directly support the wide array of sub-platforms, e.g. the diverse node.js-based tools, because:
- None of this project's developers use any tools related to node.js in any capacity except "under the hood" via Emscripten.
- The JavaScript ecosystem is more fluid than most and this project lacks the developer bandwidth to obtain and maintain expertise with the ever-changing JS landscape.
Where the "vanilla" JS does not work with with a given third-party environment we are happy to help users integrate it, but we require active cooperation from such environments' users in order to do so. Such cooperation invariably requires back-and-forth interaction with the developers as the quirks of any given environment are discovered and accommodated.
By and large, we leave the development of environment-specific builds, e.g. npm, to those who have an active interest and need for them. We offer a community-maintained npm build but cannot guaranty that it works with every combination of tools from that ecosystem. We also provide separate builds of the core JS files intended to be compatible with certain tools commonly seen in node.js-based build environments, e.g. "bundlers." See the API index for details.
It is not this project's goal to become The One True SQLite WASM Distribution and we welcome both alternative distributions and alternative implementations altogether, in particular ones which cover use cases which we lack the bandwith and topical expertise to do justice to.
Quickstart: the Canonical Builds
The canonical build files (i.e. those which live in sqlite3's own source tree) support several different build targets which might be useful to folks other than its own developers. They require a full checkout of the sqlite3 source tree (as opposed to an amalgamation distribution), GNU Make, and the Emscripten SDK. The wabt tools are also strongly recommended.
At its simplest, do the following from the top of a checked-out copy of the sqlite3 source tree:
$ ./configure --enable-all
$ make sqlite3.c
$ cd ext/wasm
$ make
Tip: a parallel build, e.g. with
make -j4
, should work just fine, but getting it to do so reliably requires running themake sqlite3.c
step in advance.
Useful targets include:
- The default target, if no target is provided, builds with
-O0
optimization level because it compiles much more quickly than any other level. o0
builds with the-O0
optimization level and may add optimzation flags which the default build does not.o1
,o2
,o3
,os
, andoz
build with the-OX
level indicated by their second character.release
is equivalent tooz
.snapshot
creates a "prelease snapshot" zip file.dist
creates a zip file suitable for use with the canonical downloads page. This differs from a "snapshot" build only in the names of the resulting zip file and embedded directory.
Much experimentation has shown that -O2
provides the fastest
binaries, but -Oz
provides the smallest (noting that
wasm-strip is required to make them small). The speed
difference between -O2
and -Oz
builds is typically only about 10%.
Each build produces a number of deliverables, including:
jswasm/sqlite3.{js,mjs,wasm}
are the core-most versions of the library and its APIs. Several other JS and WASM files are also built to this directory.sqlite3.mjs
is the same assqlite3.js
except for very minor differences required for loading it as an ES6 module.jswasm/*-bundler-friendly.*
are variant builds intended to be used with JS "bundler" tools.fiddle/fiddle-module.{js,wasm}
are the core of the fiddle application. Thefiddle/
directory can be copied as-is and served via a web server to host the sqlite3 fiddle application.
The remaining deliverables in the top-level directory are various demos and tests, neither necessary nor (in all likelihood) interesting for most folks.
64-bit Integers
JavaScript's core Number
type does not support 64-bit integers,
which causes some friction between WASM and JS regarding 64-bit
integers (whereas both support 64-bit floats).
Many sqlite3 APIs take or return int64 values, but all such APIs are
disabled unless sqlite3.js
is built with support for JavaScript's
BigInt type. This is a build-time option.
The canonical builds enable BigInt support by default. Custom builds may disable it but must be aware that all sqlite3 C APIs which take int64 arguments or return them will not be available in the JS bindings.
WASM Heap Memory
One of WASM's design features is how WASM modules are given RAM to work with. They must have one or the other of the following:
- The starting memory limit is compiled in to the WASM file.
- The memory is allocated by the client from JavaScript and provided to the WASM module loader. Unfortunately, because of how the WASM module loading process works, it is not possible to provide this from arbitrary client-level code when loading the sqlite3 module. Working around that limitation requires changes in upstream tooling, either to provide a hook for such to Emscripten or to bring the most critical Emscripten-provided features to other WASM runtimes.
Depending on how the build is configured, it may or may not be possible for the WASM module to grow its amount of memory at runtime. If it cannot, and it runs out of memory, allocation attempts will fail. Whether or not that is outright fatal to the WASM module is determined by a build option. If it is not fatal, application code must check for out-of-memory conditions on its own, as it would in C. If it is configured to be fatal, the module will fail loudly (in the browser's dev console, not necessarily someplace client-visible) and stop working if any allocation fails.
The heap memory size is one of a staggering number of build options which clients may want, or need, to customize for a given deployment. The default heap memory assigned to the module was chosen based on testing and development of the sqlite3 module. As experience is garnered via 3rd-party client applications, future releases of the module may increase the initial amount of memory the WASM module receives.
Preprocessing JS Files
In order to be able to support both conventional JavaScript and ES6
modules (a.k.a. ESM) in the same source files, we must
"preprocess" certain source files to filter specific code sections in
or out depending on whether they're being built for conventional JS or
ESM. Because C preprocessors make a mess of things when used for
preprocessing JS code, a custom preprocessor was created specifically
for use in this project, colloquially known as the C-Minus
Preprocessor, or c-pp
. It is maintained as a standalone side
project but the sqlite3 source tree contains its own
copy.
Specific JS files cannot be used as-is, and instead must be run
through c-pp
. Though this complicates the creation of custom
builds somewhat, it seems to be the "lesser evil" in terms of
approaches for maintaining the JS code in such a way as to be
usable by both flavors of JS.
Tip: files which require preprocessing have a file extension of
.c-pp.X
, whereX
is the typical extension for that file type, e.g.html
orjs
.
The preprocessor's directives do not directly interfere with the source code but do lead to constructs which, if run in JS without preprocessing them, will lead to (at best) syntax errors or (at worst) potentially silent errors. Here's an example snippet of a preprocessed block:
const W =
//#if target=es6-module
new Worker(new URL(options.proxyUri, import.meta.url));
//#else
new Worker(options.proxyUri);
//#endif
The JS keywords import
and export
trigger syntax errors in non-ESM
code, so may not be filtered out at runtime via JS's introspection
features. The capabilities they provide are essential for client-side
use of ESM modules, so cannot simply be hand-waved away as a
"nice-to-have." i.e. we have to support ESM, one way or another, and
preprocessing gives us a relatively painless way of doing so.
The preprocessor supports a configurable prefix for its keywords and
the JS code in this tree uses the prefix //#
so that such constructs
do not directly interefere with syntax highlighting and code
indentation in JS-aware text editors. The preprocessor's keywords may
not be indented: they must start on column 0.
After preprocessing, the above block is reduced to one of the following:
const W =
new Worker(new URL(options.proxyUri, import.meta.url));
or:
const W =
new Worker(options.proxyUri);
Use of the preprocessor is kept to a minimum. As of this writing, it
is used only to block of code which requires the import
ESM keyword.
All code using the preprocessor can be found by grepping the JS files
for '^//#'
.
The complete technical details of such preprocessing are maintained in
GNUMakefile
.
SQLite Encryption Extension (SEE)
The build process supports building with the SQLite Encryption Extension (SEE) variant of the SQLite amalgamation source file. See see.md for details.
Adding Client-custom Init Code
The WASM build does not support dynamically loading extensions because
there is no compatible dlopen()
equivalent for web-based WASM, but
clients may add extensions at compile time using the approach
described in this section.
If the build sees a file named sqlite_wasm_extra_init.c
in the build
directory (ext/wasm
) then the following applies:
- The build defines
SQLITE_EXTRA_INIT=sqlite3_wasm_extra_init
- That file is added to the compilation process.
- That file must define a function with this signature:
int sqlite3_wasm_extra_init(const char *)
It will be called during the library-initialization phase and passedNULL
. If the function returns non-0, initialization of the library will fail.
That function may install any extensions the client cares to, provided
they can be compiled by the WASM build process. Note that the build does not
detect and compile additional C files, so the extensions should either be
copied, in full, into sqlite3_wasm_extra_init.c
or that file should
use:
#include "/path/to/your/extension.c"
(Note the .c
extension) for each included extension.)
The file name sqlite3_wasm_extra_init.c
may be overridden by the
client by passing sqlite3_wasm_extra_init.c=/path/to/its/replacement.c
to
make
.
The source tree contains an example/template file for bootstrapping
purposes, named example_extra_init.c
. Simply rename it, edit it to
suit, and rebuild.
The C Files
Building sqlite3.wasm
requires two local C files and one local H
file, plus whatever system-level headers it requires:
sqlite3.c
andsqlite3.h
comprise the canonical amalgamation build.sqlite3-wasm.c
extends the amalgamation a small bit to add the handful of WASM-specific APIs which require C-side support. The overwhelming majority of the JS/WASM API is implemented entirely in JavaScript, with only a small amount of infrastructure-level support code implemented in C. The WASM-specific C routines are intended solely for use by the JS layer and are not considered to be public APIs unless specifically documented as such. That is, they are "unsupported" and may change at any time. If they are not documented in these pages then they are not public APIs.
When compiling, only sqlite3-wasm.c
gets compiled. It #include
's
sqlite3.c
because it requires access to some of the latter file's
internal-use-only state. It also #define
's a number of WASM-relevant
configuration flags for sqlite3.c
. Because it uses state internal to
sqlite3.c
, sqlite3-wasm.c
is only intended to be built with the a
sqlite3.c
generated from the same version of the project's source
tree.
The JS Files (sqlite3-api.js
)
The oft-mentioned sqlite3.js
is generated out of numerous other
files by a build process. This section provides an overview of those
files as a guide for those who would like to create custom builds.
Using the ext/wasm
directory as the base directory for
refering to files, the following files are used to create the JS
amalgamation, listed in the order in which they need to be assembled:
api/sqlite3-api-prologue.js
Contains the initial bootstrap setup of the sqlite3 API objects. This is exposed as a function, rather than objects, so that the next step can pass in a config object which abstracts away parts of the WASM environment, to facilitate plugging it in to arbitrary WASM toolchains.common/whwasmutil.js
A semi-third-party collection of JS/WASM utility code intended to replace much of the Emscripten glue and provide us with more control over the available JS/WASM-bridging features. The sqlite3 APIs internally use these APIs instead of their Emscripten counterparts. This API is configurable, in principle, for use with arbitrary WASM toolchains. It is "semi-third-party" in that it was created in order to support this tree but is standalone and maintained together with...jaccwabyt/jaccwabyt.js
Another semi-third-party API, jaccwabyt creates bindings between JS and C structs, such that changes to the struct state from either JS or C are visible to the other end of the connection. This is also an independent spinoff project, conceived for the sqlite3 project but maintained separately. It is used, for example, to create the OPFS sqlite3_vfs in JS.api/sqlite3-api-glue.js
Invokes functionality exposed by the previous two files to flesh out low-level parts ofsqlite3-api-prologue.js
. Most of these pieces are related to populating thesqlite3.wasm
object and using it to create more JS-friendly wrappers of the WASM-eexported functions. This file also deletes most global-scope symbols the above files create, effectively moving them into the scope being used for initializing the API.<build>/sqlite3-api-build-version.js
Gets created by the build process and populates thesqlite3.version
object.api/sqlite3-api-oo1.js
Provides a high-level object-oriented wrapper to the lower-level C API, colloquially known as OO API #1. Its API is similar to other high-level sqlite3 JS wrappers and should feel relatively familiar to anyone familiar with such APIs. That said, it is not a "required component" and can be elided from builds which do not want it.api/sqlite3-api-worker1.js
A Worker-thread-based API which uses OO API #1 to provide an interface to a database which can be driven from the main Window thread via the Worker message-passing interface. Like OO API #1, this is an optional component, offering one of any number of potential implementations for such an API.api/sqlite3-worker1.js
Is not part of the amalgamated sources and is intended to be loaded by a client Worker thread. It loads the sqlite3 module and runs the Worker #1 API which is implemented insqlite3-api-worker1.js
.api/sqlite3-worker1-promiser.js
Is likewise not part of the amalgamated sources and provides a Promise-based interface into the Worker #1 API. This is a far user-friendlier way to interface with databases which run in a separate Worker thread.
api/sqlite3-v-helper.js
This internal-use-only file installs utilities for use bysqlite3-*.js
other files which create customsqlite3_vfs
or virtual table implementations.api/sqlite3-vfs-opfs.c-pp.js
An sqlite3 VFS implementation which supports the Origin-Private FileSystem (OPFS) as a storage layer to provide persistent storage for database files in a browser. If activated, it requires...api/sqlite3-opfs-async-proxy.js
is the asynchronous backend part of the OPFS proxy. It speaks directly to the (async) OPFS API and channels those results back to its synchronous counterpart. This file, because it must be started in its own Worker, is not part of the amalgamation.
api/sqlite3-api-cleanup.js
The previous files do not immediately extend the library as they are initially evaluated. Instead they add callback functions to be called during the library's bootstrapping phase. Some also temporarily create global objects in order to communicate their state to the files which follow them. This file cleans up any dangling globals and runs the API bootstrapping process, which is what finally executes the initialization code installed by the previous files. This code ensures that the previous files leave no more than a single global symbol installed. When adapting the API for non-Emscripten toolchains, this "should" be the only file where changes are needed.
Files with the extension .c-pp.js
are intended to be processed
with c-pp
, noting that such preprocessing may be
applied after all of the relevant files are concatenated. That
extension is used primarily to keep the code maintainers cognisant of
the fact that those files contain constructs which will not run as-is
in JavaScript.
The result of concatenating those files (except for the ones noted as
being external) is typically named sqlite3-api.js
. That file
contains the entire JS API but it requires sqlite3.wasm
to be loaded
before it is bootstrapped, a process which exists primarily to
configure the JS parts to work with the current WASM environment. The
bootstrapping requires pieces specific to each WASM build environment
and happens in sqlite3-api-cleanup.js
. With a custom build it would
be possible to delay the bootstrapping further by replacing parts of
sqlite3-api-cleanup.js
, perhaps with the goal of enabling the end
client to extend the library further before bootstrapping or to
provide a more customized configuration to the bootstrap process.
sqlite3-api.js
may, depending on the build environment, be
compounded further into other JS files.
For example, in Emscripten-driven build, that file gets sandwiched
between additional files which wrap it up in such a way that the
Emscripten module-initialization process will activate it. That bundle
then gets included into the resulting Emscripten-generated
sqlite3.js
, which includes not only the sqlite3 API but also all of
the infrastructure needed for loading sqlite3.wasm
and various
utility code provided by Emscripten.
The following files are part of the build process but are injected
into the build-generated sqlite3.js
along with sqlite3-api.js
.
extern-pre-js.js
Emscripten-specific header for Emscripten's--extern-pre-js
flag. As of this writing, that file is only used for experimentation purposes and holds no code relevant to the production deliverables.pre-js.c-pp.js
Emscripten-specific header for Emscripten's--pre-js
flag. This file provides a place to override certain Emscripten behavior before it starts up.post-js-header.js
Emscripten-specific header for the--post-js
input. It opens up a lexical scope by starting a post-run handler for Emscripten.post-js-footer.js
Emscripten-specific footer for the--post-js
input. This closes off the lexical scope opened bypost-js-header.js
.extern-post-js.c-pp.js
Emscripten-specific header for Emscripten's--extern-post-js
flag. This file overwrites the Emscripten-installedsqlite3InitModule()
function with one which, after the module is loaded, also initializes the asynchronous parts of the sqlite3 module. For example, the OPFS VFS support.
Building the JS/WASM Files
Aside from the tools mentioned below, the wabt tools are also
strongly recommended, primarily for wasm-strip
.
Emscripten
The Emscripten-centric build process is covered on the Emscripten page.
wasi-sdk
sqlite3 can be built with wasi-sdk, for "server-side" use, but such builds cannot work with the provided JavaScript code because the WASI builds require JS imports which we cannot provide (but Emscripten does).
Here is a mini-HOWTO for setting up the wasi-sdk on an Ubuntu-style Linux system:
$ git clone --recursive https://github.com/WebAssembly/wasi-sdk.git
$ cd wasi-sdk
$ sudo apt install ninja-build cmake clang
$ NINJA_FLAGS=-v make package
# ^^^^ go order a pizza, wait for it to arrive, eat it, and
# check back. Maybe it'll be done by then. Maybe. As of this writing,
# it has to compile more than 3000 C++ files.
$ sudo ln -s $PWD/build/wasi-sdk-* /opt/wasi-sdk
# ^^^^^^^ /opt/wasi-sdk is a magic path name for these tools
Or, much faster, grab a prebuilt SDK binary from that git page,
unpack it somewhere, then symlink /opt/wasi-sdk
to point to it.
With that installed, the canonical source tree can be told to use it with:
$ ./configure --with-wasi-sdk=/opt/wasi-sdk
$ make
Plain clang
Like the wasi-sdk build, it is possible to compile sqlite3.wasm
using vanilla clang, but the resulting binary is not usable as-is with
this project's JavaScript code due to missing JS/WASM
imports/exports.
Building for SQLite Encryption Extension (SEE)
Since 2023-03-08, the canonical WASM build supports building against the SQLite Encryption Extension (SEE) by either:
- Adding
sqlite3-see.c
to the top-most directory of the tree before building the WASM components or - Passing
sqlite3.c=/path/to/sqlite3-see.c
when runningmake
orgmake
from theext/wasm
directory. (The path must not contain any spaces.)
The resulting jswasm/sqlite3.wasm
will include the SEE-related APIs
and jswasm/sqlite3.[m]js
will add those APIs to the sqlite3.capi
namespace, including automatic string-type argument conversion.
Caveats:
- SEE is a commercial product and a WASM build has the same distribution restrictions as binary files created using the SEE source code. This project's stance is that using SEE WASM builds within a licensee's private intranet is compatible with the SEE licensing terms, but publication on a wider network is not.
- Only encryption methods which require no third-party libraries work here. Specifically: RC4, AES128-OFB, and AES258-OFB.
- There is no known way to completely hide encryption keys from JavaScript clients. Even if such keys are compiled in to a WASM binary, those are easily disassembled to a plain-text form.
Exporting New APIs to JS
Exporting new APIs to WASM requires more than simply including them in
the sqlite3.c
amalgamation and enabling any required feature
flags. In short, it requires:
- Add them to the list of exported functions for the current build
environment. For the Emscripten-based builds, that means adding
them to
ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api
, prefixing each function with an underscore character (because Emscripten requires it). - If desirable (it normally is), adding a description of the binding
to the JS code, which ensures that it will get installed into
the
sqlite3.capi
namespace.
These steps are described below, with the caveat that ⚠ these are internal details subject to change at any time ⚠! The specifics of how C functions get exported into the WASM and JS APIs are not part of the public interface of the library and are at least partially dependent on the build platform used for creating the WASM file (i.e. part of it is Emscripten-dependent).
That said...
Extending the Exported Functions List
ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api
is a plain text file
listing all C functions which must be exported into the resulting
sqlite3.wasm
. Every function name is preceeded by an underscore
because Emscripten requires it. For maintenance's sake, the list
should be kept in alphabetical order. It is acceptable for exports to
be listed here but not exposed to JS.
If/when other build platforms are available, they may have their own function exports list.
Adding sqlite3.capi
Bindings
Once a function is exported from WASM, we can access it via the
sqlite3.wasm.exports
namespace, but such bindings are in their "raw"
form, meaning that no type conversion is performed on their arguments
or result values, e.g. for string arguments. In order to expose the
API to the public interface and add any necessary type conversion, we
must add an entry to the internal list of bindings.
⚠ ACHTUNG: again, what follows is an internal implementation detail which is subject to change at any time. Do not rely on these instructions from client-level code.
The mapping between WASM-exported functions and their JS-exposed
counterparts are stored in ext/wasm/api/sqlite3-api-glue.js
in one
of three places, depending on the role of the function and whether or
not the function uses 64-bit integers in its arguments or result
value. The bindings belong in one of the following arrays:
sqlite3.wasm.bindingSignatures
houses most functions. These bindings get mapped into thesqlite3.capi
namespace.sqlite3.wasm.bindingSignatures.int64
houses all functions which use 64-bit integers. These bindings are replaced with exception-throwing placeholders when built without BigInt support.sqlite3.wasm.bindingSignatures.wasm
houses C-side functions which are intended only for internal use within the project's own JS APIs and are not part of the public interface. These bindings get mapped into thesqlite3.wasm
namespace. Such routines which are exposed to the public API are manually re-exported to thesqlite3.capi
namespace or an equivalent wrapper is provided for them.
Each of those arrays contains sub-arrays which describe the function
arguments and return type, as documented for
sqlite3.wasm.xWrap()
. During library
initialization, a type-converting proxy for each listed function is
injected into one of the namespaces described above. If any function
listed is missing from the WASM exports, an exception will be
triggered and library initialization will fail.
When adding new entries to one of the function-list arrays, it is
important that all result and argument types refer to xWrap()
mappings, noting that a number of them are extensions defined in that
same file and are not documented in the xWrap()
docs (because they
are extensions, not core-defined conversions). Any typos or unknown
type names will lead to an exception during library initialization.