crossbind
GitHub
GUIDE · NATIVE CODE · CONANCENTER

ConanCenter

Declare a ConanCenter package in crossbind.config.js, then import one of its headers with the conan: prefix. crossbind builds the package from source with its own toolchains, links it statically and binds the header as it binds a port's.

What you need

With the default Docker runner, nothing to install for web, Android, Linux and Windows builds: the toolchain images carry Conan. iOS and macOS packages build on your Mac with Xcode whatever the runner, so those need Conan 2.19 or later and CMake on the PATH. The first build of a package needs the network, to reach ConanCenter and the sources its recipe downloads.

Under RUNNER=LOCAL, web builds need Conan 2.19 or later, Emscripten and CMake on the PATH, and Android, Linux and Windows packages are refused: they need the NDK, the sysroots and llvm-mingw of the images.

Declare the package

crossbind.config.js
export default {
conanDependencies: {
zlib: '1.3.2',
libpng: '[>=1.6 <2]',
sqlite3: { version: '3.53.4', options: { enable_fts5: true } },
},
paths: { config: import.meta.url },
};
  • A key is the Conan package name. Its value is a version, a Conan version range, or { version, options } with options the recipe defines, as strings, numbers or booleans.
  • shared is not an option you can set: every package links statically into the module.

Import a header

src/main.js
import { initNative, zlibVersion, compressBound } from 'conan:zlib/zlib.h';
import { png_access_version_number } from 'conan:libpng/png.h';
 
await initNative();
console.log(await zlibVersion(), await compressBound(1000));

conan:<package>/<header> names a header under the package's include directory, subdirectories included: conan:libxml2/libxml/parser.h. Importing a package that conanDependencies does not declare fails the build. A package another one requires - zlib under libpng - is built and linked either way, and importable once you declare it.

The header binds as any library header does: its classes and enums whole, a free function only when your code imports it by name. Without a bundler, Node.js resolves conan: imports through the module hooks crossbind build -e node writes (Node.js).

Through your own header

The include directories of every package are on your native sources' include path, so a header of your own can use any of them, C++ libraries included. It is also the way to reach what a header cannot bind on its own, such as templates and header-only libraries:

src/native/text.h
#pragma once
 
#include <string>
#include <fmt/format.h>
 
class Text {
public:
static std::string price(double value) { return fmt::format("{:.2f}", value); }
};
src/main.js
import { initNative, Text } from './native/text.h';
 
await initNative();
console.log(await Text.price(3.14159));

C++ exceptions cross between your code and a package: an fmt::format_error thrown inside libfmt reaches your catch. Your sources also compile with the defines a recipe declares for code that uses it (cpp_info.defines), such as libcurl's CURL_STATICLIB on Windows.

React Native and Node.js

The same imports work in a React Native app on Android and iOS; the native build installs the packages before Metro bundles the bridges. Android packages are built for API level 33, as the ports are, so in an app with a lower minSdkVersion a package that calls a newer libc function fails to link. iOS packages are built for device and simulator, arm64 and iOS 15.1, and each library becomes an xcframework the react-native-crossbind pod vendors - run pod install again after you change conanDependencies.

A Node.js addon for macOS, Linux or Windows links the packages built for its own target, with the system libraries and Apple frameworks their recipes ask for. A package's symbols stay inside the addon, as a port's do, so they never trade calls with the zlib and OpenSSL that Node.js exports itself.

What a build does

  1. When a build starts - in a bundler plugin, crossbind build, or the Gradle or Xcode build of a React Native app - crossbind runs conan install for each release target it needs, with a profile written from the toolchain the ports are built with. Every package is a static library.
  2. ConanCenter publishes no binaries for these toolchains, so each package builds from source the first time. Built packages stay in ~/.crossbind/conan/store, shared by every project; iOS, macOS and RUNNER=LOCAL builds use ~/.crossbind/conan-local/store.
  3. Each package is staged in .crossbind/conan/packages/<package>/dist/prebuilt/<target>/, the layout a port ships, and linked like one. A debug build links the release archives.
  4. A target is installed again only when its inputs change: conanDependencies, the toolchain, crossbind's profile or conan.lock.

Each install writes Conan's whole log to .crossbind/conan/logs/<target>.log; a failed one also prints its end.

What a recipe can reach

A recipe is Python that runs the package's own build system, so crossbind fences it in as far as the runner allows:

  • Each conan install gets a work directory and a Conan home of its own, deleted when it ends: plugins, hooks, remotes or settings a recipe writes never reach the next install.
  • In the default Docker runner the container sees the package store and that work directory, not your project, and every path it reports back is checked before anything is copied. A remote runner sends back only the built packages.
  • The store is shared and writable, so a recipe built for one project can change what another later takes from it. Delete ~/.crossbind/conan/store to build every package again.
  • Under RUNNER=LOCAL, and for iOS and macOS on any runner, recipes run on your machine with your permissions.

The lockfile

The first install writes conan.lock next to crossbind.config.js. It pins the exact version and recipe revision of every package, so commit it. Later installs follow it and add any requirement it does not list yet; delete it to resolve every version again.

TypeScript

Each conan: import is typed by an ambient module, declare module 'conan:zlib/zlib.h', under .crossbind/conan/types/, which @crossbind/typescript-config includes; a tsconfig with its own include needs .crossbind/conan/types/**/*.d.ts in it. The declarations list every bindable function of the header, even before you import it, but only the imported ones enter the compiled bridge.

Licences

crossbind licenses lists every Conan package of the build with the licence its recipe declares, the source URL and SHA-256 the recipe downloads, and the licence texts the package ships; in the SBOM a package is pkg:conan/<package>@<version>. It reads what the last build installed, so on a fresh checkout it stops with an error until you build (CLI).

Limits

  • No WASI: Conan has no WASI target, and a crossbind build that would also build WASI fails while conanDependencies is declared - name your platforms with -p.
  • A recipe that does not build for Emscripten, the NDK, iOS, macOS, a Linux sysroot or llvm-mingw fails with Conan's own error; its options can often switch the failing part off.
  • Web and React Native builds link a package's own archives only: the system libraries and Apple frameworks its recipe asks for are not added.
  • A header binds as far as a port's does: C APIs bind, variadic functions when imported by name, templates do not.
  • Packages come from ConanCenter only; another remote, or a login to one, is not supported.
Type to search every guide page and section.
↑↓ navigate↵ openesc close