Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ test/cljs/**/*.mjs
# dtype-next Panama upcall stubs generated at runtime (gensym per interface)
classes/
.clj-kondo/.cache-cli/
dist/
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
# Changelog

## [Unreleased]

### Added

- `:float32`, a C float, as an argument or a return type.
- `ffi-wasm/handler`, the worker side of ffi-wasm. `init-pool!` gives its
URL to each handler as `ffiWasmHandlerUrl`.
- `:label` for `gen-handler-source`.

### Changed

- ffi-wasm ships its own modules in `dist/`: the page bundle
`ffi-wasm.mjs`, and `handler.mjs` and the modules that the two share as
files of their own, one copy of each. `./handler`, `./handler-*`,
`./http-bridge`, `./fetch-worker` and `./test-runner` point at their own
files, and each other subpath at `ffi-wasm.mjs`. Node and a bundler resolve
the same subpaths as before. Of `src/`, the package ships only
`macros.cljc` and `macros.mjs`.
- A page maps `ffi-wasm` to `dist/ffi-wasm.mjs`, and each subpath that it
imports to the file of that subpath. It also maps `worker-router`,
`worker-router/worker-bootstrap`, `comlink`, `resource-tracker` and
`squint-cljs/`.
- Worker code imports `ffi-wasm/handler` or a worker-side subpath, not
`ffi-wasm`. A bundler of a `dist/` file marks `node:*` as external.
- **Breaking.** `stage-test-deps!` takes `:native-dist`, the `dist/` dir of
the installed ffi-wasm, in place of `:native-src`, and copies only
`test_runner.mjs`.
- **Breaking.** A generated handler has no static import of ffi-wasm. It
imports ffi-wasm from `ffiWasmHandlerUrl`. Its overrides module exports
`methods(ffi)`, and `init` gets `ctx.ffi`. `:runtime-import-path` is gone.
- On Node, `createSyncFetch` defaults `workerUrl` to its own
`fetch_worker.mjs`.

### Fixed

- The JVM writes the classes that dtype-next generates to a temp dir. A
project gets no `./classes` dir.
- A zig `-O0` build links no UBSan runtime.

## [0.0.2] - 2026-09-28

### Added
Expand Down Expand Up @@ -41,3 +80,7 @@
## [0.0.1] - 2026-07-30

First release.

[Unreleased]: https://github.com/willcohen/clj-native/compare/0.0.2...HEAD
[0.0.2]: https://github.com/willcohen/clj-native/compare/0.0.1...0.0.2
[0.0.1]: https://github.com/willcohen/clj-native/releases/tag/0.0.1
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,37 @@ host.
| C struct to map | `graal-wasm/read-struct` | none |

`heapHelpers(getModule)` returns an object of methods. Spread it into the
handler's `methods` map, before the consumer's own keys. A consumer key with
the same name replaces the helper.
object that the `methods(ffi)` of the overrides module returns, before the
consumer's own keys. A consumer key with the same name replaces the helper.

On JavaScript, a consumer writes its string-array and struct reads in its own
overrides module, which knows the struct layouts of its library. On the JVM,
`read-struct` takes the layout as an argument.

## Packages

The npm package [`ffi-wasm`](https://www.npmjs.com/package/ffi-wasm) holds the
hand-written `.mjs` runtime helpers and the squint output of the `.cljc` and
`.cljs` modules. The `exports` map in `package.json` lists them. The package
also holds `macros.cljc`, because squint expands the macros at compile time.
The npm package [`ffi-wasm`](https://www.npmjs.com/package/ffi-wasm) ships
its own modules in `dist/`. `ffi-wasm.mjs` is an esbuild bundle of the page
side. `handler.mjs` holds the worker side. The modules that the two share are
files of their own, and each importer imports them by a relative path, so a
page or a Node process loads one copy of each. `./handler`, `./handler-*`,
`./http-bridge`, `./fetch-worker` and `./test-runner` point at their own
files, and each other subpath points at `ffi-wasm.mjs`. `dist/` holds no other
package: a page maps `ffi-wasm`, `worker-router`,
`worker-router/worker-bootstrap`, `comlink`, `squint-cljs/` and
`resource-tracker` in its importmap, one copy of each. A page that imports a
subpath also maps it to the file of that subpath.

A module worker does not use the importmap of the page. `handler.mjs` and the
shared modules import only each other and `node:` builtins, and `init-pool!`
gives each handler the URL of `handler.mjs`. Worker code imports
`ffi-wasm/handler` or a worker-side subpath, not `ffi-wasm`. A bundler of
worker code marks `node:*` as external. The `gen-handler-source` docstring
gives the contract of the overrides module.

Besides `dist/`, the package holds `macros.cljc`, because squint expands the
macros at compile time, and `macros.mjs`, which the squint output of a
consumer imports by path.

The jar [`net.willcohen/native`](https://clojars.org/net.willcohen/native)
holds the source of these 13 namespaces, and a clj-kondo config export. It
Expand Down
7 changes: 4 additions & 3 deletions bb.edn
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,16 @@ Drift fails build:js, and through prepack each npm pack."
:task (nb/check-exports-sync! "package.json")}

build:js
{:doc "Compile each shipped .cljc or .cljs module to the .mjs that npm ships."
{:doc "Compile each .cljc or .cljs module to .mjs, then bundle dist/ with esbuild.config.mjs."
:depends [check:exports npm:install]
:task (let [dir "src/cljc/net/willcohen/native"]
(doseq [f nb/shipped-module-files
(doseq [f nb/export-module-files
:let [base (str/replace f (re-pattern "\\.mjs$") "")
src (first (filter (fn [src] (fs/exists? (fs/path dir src)))
[(str base ".cljc") (str base ".cljs")]))]
:when src]
(nb/squint-compile! dir src)))}
(nb/squint-compile! dir src))
(shell "node" "esbuild.config.mjs"))}

build:js:test
{:doc "Compile each cljs.test suite."
Expand Down
59 changes: 59 additions & 0 deletions esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2026 Will Cohen
//
// Part of clj-native, under the Apache License v2.0 with LLVM Exceptions.
// See LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// Builds dist/: the page bundle of ffi-wasm's own modules, and next to it the
// worker entry, the modules that the page and the worker share, and the files
// that createSyncFetch and a test load. bb build:js runs it after squint
// compiles the .cljc modules.
import * as esbuild from 'esbuild';
import { copyFileSync, mkdirSync } from 'node:fs';

const src = 'src/cljc/net/willcohen/native';

// Each is a file of its own in dist/, and each importer imports it by a
// relative path, so a realm that loads the page bundle and handler.mjs loads
// one copy of each. They import only each other and node: builtins, thus a
// module worker needs no importmap for them.
const shared = ['handler_runtime', 'handler_env', 'handler_fs', 'handler_heap',
'handler_paths', 'http_bridge'].map((m) => `${m}.mjs`);

const keepSharedExternal = {
name: 'keep-shared-external',
setup(build) {
build.onResolve({ filter: /^\.\/[a-z_]+\.mjs$/ }, (args) =>
shared.includes(args.path.slice(2)) ? { path: args.path, external: true } : undefined);
},
};

const common = {
bundle: true,
format: 'esm',
platform: 'neutral',
mainFields: ['module', 'main'],
// Node loads the builtins through dynamic imports on Node only. Each one
// has the node: prefix, so a consumer that bundles a dist file marks only
// node:* as external.
external: ['node:*'],
logLevel: 'warning',
};

mkdirSync('dist', { recursive: true });

await esbuild.build({
...common,
entryPoints: [`${src}/ffi_wasm.mjs`],
outfile: 'dist/ffi-wasm.mjs',
// The bundle holds ffi-wasm's own code only. A copy of another package is a
// second module instance next to the copy that a consumer imports.
external: [...common.external, 'squint-cljs', 'resource-tracker', 'worker-router'],
plugins: [keepSharedExternal],
});

// test_runner.mjs is out of the page bundle, so that a page does not import
// the squint test library.
for (const f of ['handler.mjs', ...shared, 'fetch_worker.mjs', 'test_runner.mjs']) {
copyFileSync(`${src}/${f}`, `dist/${f}`);
}
3 changes: 2 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// Lints the hand-written .mjs and the test fixtures. The ignored src files are
// gitignored squint outputs. test/cljs and test/cljc hold compiled test
// gitignored squint outputs, and dist/ holds the esbuild bundles. test/cljs and test/cljc hold compiled test
// mirrors.
import js from '@eslint/js';
import globals from 'globals';
Expand All @@ -19,6 +19,7 @@ export default [
'src/cljc/net/willcohen/native/pool.mjs',
'src/cljc/net/willcohen/native/test_runner.mjs',
'src/cljc/net/willcohen/native/workload_pool.mjs',
'dist/**',
'test/cljs/**',
'test/cljc/**',
],
Expand Down
Loading
Loading