Liking cljdoc? Tell your friends :D

clj-proj

Live Demo

CI NPM Version Clojars Version

This project provides a native (or transpiled) version of PROJ (GitHub) for both the JVM and JS ecosystems.

The goal of this project is to provide a long-missing component of geospatial analysis for these platforms: a performant version of PROJ that can closely follow upstream development.

This currently provides bindings to the JVM via Clojure using a package published to Clojars, and to pure Javascript via an ES6 module called proj-wasm published to NPM, which provides a clean interface to an internal transpiled WASM module.

EARLY DEVELOPMENT

This project is in its initial phases, with partial functionality built out, and incomplete testing. Feedback from testers is welcome and encouraged.

Consider all APIs and structuring of this library to be an early work-in-progress, subject to potentially substantial change while basic functionality continues to be developed.

How It Works

clj-proj provides PROJ functionality through a multi-implementation architecture that automatically selects the best available backend at runtime:

API Scope

This library currently focuses on PROJ's C API for ISO 19111 functionality. PROJ's C API is split into several sections, and objects from the ISO 19111 section generally do not mix with functions from other sections (and vice versa). ISO 19111 CoordinateOperation objects that can be exported as valid PROJ pipelines are the exception — these work with transformation functions like proj_trans_array().

Implementation Strategy

The library provides a unified API (net.willcohen.proj.proj) that automatically selects the best available backend at runtime:

  1. Native FFI (via JNA) - Direct calls to compiled PROJ libraries
  2. GraalVM WebAssembly - Runs emscripten-compiled PROJ in JVM
  3. JavaScript/WebAssembly - Direct WASM for Node.js and browsers

During initialization, the library detects the environment and available backends:

;; The implementation atom tracks which backend is active
(def implementation (atom nil))

;; JVM: tries native FFI first, falls back to GraalVM WASM
;; CLJS: initializes worker pool, returns a Promise
(defn init! []
  #?(:clj
     (if @force-graal
       (do (wasm/init-proj) (reset! implementation :graal))
       (try
         (native/init-proj)
         (reset! implementation :ffi)
         (catch Throwable e
           (wasm/init-proj)
           (reset! implementation :graal))))
     :cljs
     (-> (wasm/init-proj opts)
         (.then (fn [_] (reset! implementation runtime))))))

Runtime Dispatch System

The library uses a runtime dispatch architecture where all PROJ functions flow through a central dispatcher:

;; Macros generate thin wrapper functions
(defn proj-create-crs-to-crs [opts]
  (dispatch-proj-fn :proj_create_crs_to_crs 
                    (get fndefs :proj_create_crs_to_crs) 
                    opts))

;; The dispatcher orchestrates the entire call flow
(defn dispatch-proj-fn [fn-key fn-def opts]
  (ensure-initialized!)
  (let [args (extract-args (:argtypes fn-def) opts)
        result (dispatch-to-platform-with-args fn-key fn-def args)]
    (process-return-value-with-tracking result fn-def)))

The dispatch system handles:

  • Argument extraction: Converting Clojure maps to function arguments with defaults
  • Platform routing: Sending calls to the appropriate backend implementation
  • Return processing: Converting C types back to Clojure data structures
  • Context management: Ensuring thread-safe access to PROJ contexts

Resource Management

PROJ returns pointers that must be freed. The library automatically tracks and cleans up these resources:

  • JVM: Uses tech.v3.resource for automatic cleanup during garbage collection
  • JavaScript: Uses resource-tracker library for automatic cleanup
;; When a function returns a pointer, it's automatically tracked internally
;; No manual cleanup needed - this happens behind the scenes:
(resource/track result-pointer
  {:dispose-fn (fn [] (proj-destroy pointer-address))
   :track-type :auto})  ; Cleaned up on GC

You never need to call proj-destroy or similar cleanup functions manually. All resources are automatically cleaned up when they go out of scope or during garbage collection.

Context Management

PROJ uses contexts for thread safety and operation tracking. The library provides flexible context handling:

;; Use an explicit context (stored in an atom)
(def ctx (context-create))
(proj-get-authorities-from-database {:context ctx})

;; Or let the library create a temporary context
(proj-get-authorities-from-database {})  ; Creates context internally

In JavaScript with the worker pool, contexts are pinned to specific workers. When PJ objects from different workers are passed to the same function (e.g., after round-robin context creation), the library automatically reconciles them by recreating mismatched objects on the target worker via PROJJSON roundtrip. A console.warn is emitted when this happens — for best performance, use an explicit shared context.

For functions that require atomic context access, the library uses the cs (context-swap) wrapper:

  • Ensures thread-safe access by wrapping operations in an atom's swap!
  • Tracks operation counts and results
  • Handles platform-specific context requirements

Context atoms maintain state including:

  • The native context pointer
  • Operation counter for tracking calls
  • Result storage for atomic operations

Coordinate Transformation Implementation

The library provides efficient handling of both single and batch coordinate transformations:

;; Single coordinate transformation
(trans-coord transformation [longitude latitude])

;; Batch transformation with coordinate arrays
(def coords (coord-array 1000 2))  ; 1000 2D coordinates
(set-coords! coords [[lon1 lat1] [lon2 lat2] ...])
(proj-trans-array {:P transformation :coord coords :n 1000})

Coordinate arrays are implemented differently per platform:

  • FFI: Uses dtype-next tensors for zero-copy native memory access
  • GraalVM: Allocates memory in the WASM heap
  • ClojureScript: Worker-allocated arrays via message passing

Advanced Features

Dynamic Implementation Switching

;; Force a specific implementation for testing
(force-graal!)  ; Use GraalVM even if native is available
(force-ffi!)    ; Use native FFI

;; Check current implementation
(ffi?)    ; => true if using native
(graal?)  ; => true if using GraalVM

Cross-Platform Testing

The test framework ensures consistent behavior across all implementations:

(defmacro with-each-implementation [& body]
  ;; Runs the same test against FFI, GraalVM, and ClojureScript
  ...)

Flexible API

The library provides a developer-friendly API with several conveniences:

  • Parameter naming flexibility: Use either underscores or hyphens

    ;; Both work identically:
    (proj-create-crs-to-crs {:source_crs "EPSG:4326" :target_crs "EPSG:2249"})
    (proj-create-crs-to-crs {:source-crs "EPSG:4326" :target-crs "EPSG:2249"})
    
  • Optional parameters with defaults: Functions provide sensible defaults

    ;; Context is optional - library creates one if needed
    (proj-get-authorities-from-database {})
    
    ;; Or provide your own
    (proj-get-authorities-from-database {:context my-ctx})
    
  • Idiomatic return keys per platform: Maps returned by out-param and struct-list functions use platform-native key casing:

    • Clojure: kebab-case keywords (:west-lon-degree, :semi-major-metre)
    • Java: camelCase strings ("westLonDegree", "semiMajorMetre")
    • JS camelCase aliases: camelCase keys (westLonDegree, semiMajorMetre)
    • JS snake_case aliases: snake_case keys (west_lon_degree, semi_major_metre)
  • Consistent error handling: All platforms handle errors uniformly

    • C++ exceptions from WASM are caught and converted
    • Native errors are wrapped in Clojure exceptions
    • Helpful error messages across all backends

Grid Fetching

PROJ transformations can require grid shift files -- TIFF-format datum corrections hosted at cdn.proj.org. Without these grids, transformations like NAD27->NAD83 degrade to "ballpark" accuracy. clj-proj fetches grids automatically when network access is enabled.

Per-Platform Behavior

  • JVM + Native FFI: Java's HttpClient handles HTTP range requests via JNA callbacks. No configuration needed.
  • JVM + GraalVM WASM: Java's HttpClient fetches grids via a C stub bridge (proj_network_stubs.c + network.clj). No configuration needed.
  • Browser: PROJ runs in Web Workers where Emscripten's synchronous FETCH (via Atomics.wait) is allowed. Works without special headers; pthreads mode requires Cross-Origin Isolation (see below).
  • Node.js: PROJ runs in worker_threads with an XMLHttpRequest polyfill that delegates sync requests to a fetch-worker via SharedArrayBuffer + Atomics. No configuration needed.

Enabling/Disabling Network

;; Network enabled by default
(def ctx (proj/context-create))

;; Explicitly disable network
(def ctx-offline (proj/context-create {:network false}))
// JavaScript - context is optional; create one explicitly to control network
const ctx = await proj.contextCreate();              // network enabled (default)
const ctxOffline = await proj.contextCreate({network: false}); // network disabled

Browser: Cross-Origin Isolation

The library supports two worker modes in browsers:

  • Single-threaded mode (fallback): Each worker runs the same WASM binary but without internal threading. No special headers needed.
  • Pthreads mode: With SharedArrayBuffer available, Emscripten can spawn internal pthreads within each worker for additional parallelism. Requires Cross-Origin Isolation headers:
    • Cross-Origin-Opener-Policy: same-origin
    • Cross-Origin-Embedder-Policy: require-corp

The coi-serviceworker.js included in the demo page enables pthreads mode on static hosting (e.g. GitHub Pages) but is not required.

Known Limitations

  • GraalVM network callbacks add initialization overhead

Platform-Specific Details

JVM (Java / Clojure)

The JVM implementation supports two backends:

  1. Native FFI (Preferred) - Available on supported platforms
  2. GraalVM WebAssembly (Fallback) - For platforms without native libraries

Currently supported platforms (native):

  • macOS/darwin Apple Silicon (arm64)
  • Linux x64 and arm64
  • Windows x64

Not yet built:

  • macOS/darwin Intel (x86_64)
  • Windows ARM64 - Cross-compiler not available in nixpkgs

JDK 21+ with native library

On platforms with a native precompiled PROJ available, this library utilizes JNA via dtype-next. This is the preferred option. If dtype-next adopts Panama FFM, this library may follow.

How Native FFI Works

The native implementation:

  1. Extracts platform-specific libraries from resources to a temp directory
  2. Configures JNA to load from that directory
  3. Uses dtype-next for efficient native interop and memory management

The library includes pre-compiled PROJ libraries for each platform in resources/{platform}/. At runtime, it detects the OS and architecture, then loads the appropriate libraries.

Usage

On a computer where the native library was built:

(require '[net.willcohen.proj.proj :as proj])

;; Initialization happens automatically on first use in Clojure/JVM
;; For explicit initialization, you can call:
;; (proj/init!)  ; Primary function
;; (proj/init)   ; Convenience alias (same as init!)

;; Create a coordinate transformation
(def ctx (proj/context-create))
(def transformer (proj/proj-create-crs-to-crs {:context ctx
                                               :source-crs "EPSG:4326"
                                               :target-crs "EPSG:2249"}))

;; Transform a single coordinate 
(def coords (proj/coord-array 1))
;; EPSG:4326 uses lat/lon order, not lon/lat!
(proj/set-coords! coords [[42.3603222 -71.0579667]]) ; Boston City Hall (lat, lon)
(proj/proj-trans-array {:P transformer :coord coords :n 1})
;; coords now contains transformed coordinates in EPSG:2249 (MA State Plane)

;; Query available authorities
(proj/proj-get-authorities-from-database)
;; => ["EPSG" "ESRI" "PROJ" "OGC" ...]

;; No manual cleanup needed! Resources are automatically tracked and 
;; cleaned up when they go out of scope or during garbage collection

Java API

A Java wrapper class (net.willcohen.proj.PROJ) provides idiomatic Java access to the library:

import net.willcohen.proj.PROJ;

// Initialize (auto-selects best backend: native FFI or GraalVM WASM)
PROJ.init();

// Create a context and transformation
Object ctx = PROJ.contextCreate();
Object transform = PROJ.createCrsToCrs(ctx, "EPSG:4326", "EPSG:2249");

// Transform coordinates (EPSG:4326 uses lat/lon order)
Object coords = PROJ.coordArray(1);
PROJ.setCoords(coords, new double[][]{{42.3603222, -71.0579667}}); // Boston City Hall
PROJ.transArray(transform, coords, 1);
// coords now contains transformed coordinates in EPSG:2249 (MA State Plane)

// Query available authorities
List<String> authorities = PROJ.getAuthoritiesFromDatabase();
// => ["EPSG", "ESRI", "PROJ", "OGC", ...]

// Create transformation from CRS objects (for advanced use)
Object sourceCrs = PROJ.createFromDatabase(ctx, "EPSG", "4326");
Object targetCrs = PROJ.createFromDatabase(ctx, "EPSG", "2249");
Object transformFromPj = PROJ.createCrsToCrsFromPj(ctx, sourceCrs, targetCrs);

// No manual cleanup needed - resources are automatically tracked!

The Java API mirrors the Clojure API and supports:

  • All initialization and backend control methods (init(), forceGraal(), forceFfi())
  • Context management (contextCreate(), isContext())
  • CRS transformations (createCrsToCrs(), createCrsToCrsFromPj(), createFromDatabase())
  • Coordinate arrays (coordArray(), setCoords(), transArray())
  • Database queries (getAuthoritiesFromDatabase(), getCodesFromDatabase())
  • CRS introspection (getAreaOfUse(), ellipsoidGetParameters(), csGetAxisInfo(), primeMeridianGetParameters(), coordoperationGetMethodInfo(), etc.) -- C output parameters are handled automatically, returning Maps
  • Direction constants (PJ_FWD, PJ_INV, PJ_IDENT)

JDK 21+ with GraalVM WebAssembly

On platforms where no native library is available, this library falls back to running the WebAssembly transpiled version of PROJ through GraalVM's WebAssembly support.

Users needing this transpiled PROJ must use at least JDK 21 due to GraalVM's requirements and should enable JVMCI to improve performance.

How GraalVM Implementation Works

When native libraries aren't available, the GraalVM implementation:

  1. Creates a GraalVM polyglot context with JavaScript and WebAssembly support
  2. Loads the emscripten-compiled PROJ module, then writes proj.db and proj.ini to Emscripten's virtual filesystem
  3. Manages type conversion between JVM and JavaScript using ProxyArray, ProxyExecutable, and ProxyObject
  4. Bridges PROJ's network callbacks through C stubs (proj_network_stubs.c) that dispatch to Java-side ProxyExecutable callbacks (network.clj) via globalThis, enabling grid fetching through Java's HttpClient
  5. Handles C++ exceptions from WASM code gracefully

Note: GraalVM may print "WARNING: The polyglot context is using an implementation that does not support runtime compilation" during initialization. This is expected behavior indicating interpreted (non-JIT) WASM execution and does not affect correctness.

The main challenge is initialization performance -- loading the WASM binary (3.6MB) and PROJ database (9.4MB) takes several seconds.

Usage

To force GraalVM implementation on a system where native libraries are available:

(require '[net.willcohen.proj.proj :as proj])

;; Force GraalVM WASM implementation
;; If on a fallback-only platform, this step is unneeded
(proj/force-graal!)
;; => true

;; Usage is identical to native implementation
(def ctx (proj/context-create))
(def transformer (proj/proj-create-crs-to-crs {:context ctx
                                               :source-crs "EPSG:4326"
                                               :target-crs "EPSG:2249"}))

;; Transform coordinates (EPSG:4326 uses lat/lon order)
(def coords (proj/coord-array 1))
(proj/set-coords! coords [[42.3603222 -71.0579667]]) ; Boston City Hall
(proj/proj-trans-array {:P transformer :coord coords :n 1})
;; coords now contains transformed coordinates

;; No manual cleanup needed - resources are automatically managed!

Note: GraalVM initialization takes 5-7 seconds as it loads the WASM module. You may see Truffle/GraalVM diagnostic output during initialization.

JavaScript / ClojureScript

The JavaScript implementation uses emscripten-compiled PROJ running in workers:

  • Worker-based WASM execution: PROJ runs in Web Workers (browser) or worker_threads (Node.js), keeping the main thread responsive and allowing synchronous network operations for grid fetching
  • Cherry-cljs compilation: ClojureScript code compiles to clean ES6 modules
  • Async API: All operations return Promises since they dispatch to workers

Environment-Specific Behavior

The library automatically detects and adapts to different JavaScript environments:

  • Node.js: PROJ runs in worker_threads with an XMLHttpRequest polyfill for grid fetching
  • Browser: PROJ runs in Web Workers with Emscripten's built-in FETCH support
  • Environment detection: Automatic at initialization

Usage

For Node.js, create index.mjs:

import * as proj from "proj-wasm";

// Initialize PROJ (required before any operations in JavaScript)
await proj.init();  // Convenience alias for init! (also available as init_BANG_)

// Create a transformation (all operations are async, context is auto-created)
const transformer = await proj.projCreateCrsToCrs({
  source_crs: "EPSG:4326",
  target_crs: "EPSG:2249"
});

// Transform coordinates (EPSG:4326 uses lat/lon order)
const coords = await proj.coordArray(1);
await proj.setCoords(coords, [[42.3603222, -71.0579667, 0, 0]]); // Boston City Hall (lat, lon)
await proj.projTransArray({
  p: transformer,
  direction: proj.PJ_FWD,
  n: 1,
  coord: coords
});

// Read transformed coordinates
const transformed = await proj.getCoords(coords, 0);
console.log("Transformed:", transformed[0], transformed[1]);

// Shutdown workers when done (allows Node.js process to exit cleanly)
await proj.shutdown();

// Resources are automatically cleaned up - no manual cleanup needed!
// The resource-tracker library handles cleanup when objects go out of scope

// Optional: create an explicit context to disable network or pin to a worker
// const ctx = await proj.contextCreate({ network: false });
// const t = await proj.projCreateCrsToCrs({ context: ctx, ... });

For browsers, the same API works. See the Grid Fetching section for Cross-Origin Isolation requirements when using pthreads mode.

$ node index.mjs
# Transformed coordinates will be displayed

clj-proj Build Guide

Prerequisites

Docker/Podman users:

  • Install Docker or Podman
  • Resource requirements: 150GB disk, 8GB RAM
  • Podman setup: podman machine init --disk-size 150 --memory 8192

Babashka + Nix users:

  • Install Nix and direnv
  • One-time setup: direnv allow

Building

Quick Reference

bb tasks          # List all available commands
bb build --help   # Show build options
bb clean --help   # Show clean options

Docker/Podman users: First build the development container:

docker build --target dev -t clj-proj:dev .
# or: podman build --target dev -t clj-proj:dev .

Rebuild the container when:

  • Containerfile changes
  • flake.nix dependencies change
  • After pulling latest changes that modify build environment
  • Add --no-cache flag to force complete rebuild if needed

Common Build Tasks

Native libraries (current platform):

# Babashka + Nix
bb build --native

# Docker/Podman alternative  
docker run --rm -v $(pwd):/workspace clj-proj:dev bb build --native

WebAssembly build:

# Babashka + Nix  
bb build --wasm

# Docker/Podman alternative
docker run --rm -v $(pwd):/workspace clj-proj:dev bb build --wasm

Cross-platform builds:

# Babashka + Nix (uses Docker internally)
bb build --cross-platform linux/amd64     # Working
bb build --cross-platform linux/aarch64   # Working
bb build --cross-platform windows/amd64   # Working
bb build --cross                          # Build all default platforms

# Docker/Podman direct
docker build --platform linux/amd64 --target export --output type=local,dest=./artifacts .
docker build --platform linux/arm64 --target export --output type=local,dest=./artifacts .

Complete build + test:

# Babashka + Nix
bb test-run       # Builds everything, runs all tests

# Docker/Podman
docker build --target test-all .

Development Setup

Babashka + Nix:

direnv allow      # One-time setup
bb dev            # Rich REPL with Portal
bb demo           # Browser demo at localhost:8080

Docker/Podman:

# First-time setup: build the development container
docker build --target dev -t clj-proj:dev .

# Then start interactive development environment
docker run -it --rm -v $(pwd):/workspace -p 7888:7888 -p 8080:8080 clj-proj:dev
# Inside container: bb dev, bb demo, etc.

Packaging

JVM (JAR file):

# Babashka + Nix
bb jar

# Docker/Podman  
docker run --rm -v $(pwd):/workspace clj-proj:dev bb jar

JavaScript (ES6 module):

# Babashka + Nix
bb cherry

# Docker/Podman
docker run --rm -v $(pwd):/workspace clj-proj:dev bb cherry

Build Process Overview

  1. Native builds compile PROJ + dependencies (SQLite, LibTIFF, zlib) for the host platform

    • Output: resources/{platform}/ (e.g., resources/darwin-aarch64/)
    • Linux: Static linking
    • Windows: Static linking
  2. WASM builds use emscripten to compile PROJ into WebAssembly

    • Output: resources/wasm/ and src/cljc/net/willcohen/proj/
    • Requirements: emscripten tools in PATH (automatically provided in containers)
  3. Cross-platform builds use Docker containers with Nix for reproducible builds

    • Requirements: Docker or Podman installed
    • Resource requirements: 150GB disk, 8GB RAM
    • Podman setup: podman machine init --disk-size 150 --memory 8192

Testing

Run all tests:

# Babashka + Nix  
bb test:all

# Docker/Podman
docker build --target test-all .

Test specific implementations:

# Native FFI
bb test:ffi                    # Babashka + Nix
docker run --rm -v $(pwd):/workspace clj-proj:dev bb test:ffi

# GraalVM WebAssembly  
bb test:graal                  # Babashka + Nix
docker run --rm -v $(pwd):/workspace clj-proj:dev bb test:graal

# JavaScript/Node.js
bb test:node                   # Babashka + Nix
docker run --rm -v $(pwd):/workspace clj-proj:dev bb test:node

# Browser integration
bb test:playwright             # Babashka + Nix (requires display)
# (Not available in headless containers)

Test packaged artifacts:

# JAR as downstream dependency
bb test:jar                    # Babashka + Nix
docker run --rm -v $(pwd):/workspace clj-proj:dev bb test:jar

# npm package as downstream dependency
bb test:npm                    # Babashka + Nix  
docker run --rm -v $(pwd):/workspace clj-proj:dev bb test:npm

# Linux cross-platform testing
bb test:linux                  # Uses Docker internally

The test framework runs identical tests against all implementations, ensuring consistent behavior across platforms.

Architecture Notes

File Organization

clj-proj/
├── src/
│   ├── c/
│   │   └── proj_network_stubs.c        # GraalVM WASM network callback stubs
│   ├── clj/net/willcohen/proj/impl/    # JVM-specific implementations
│   │   ├── native.clj                  # JNA/FFI bindings
│   │   ├── logging.clj                 # PROJ log callback
│   │   ├── network.clj                 # GraalVM WASM grid fetching
│   │   └── struct.clj                  # Native struct definitions
│   ├── cljc/net/willcohen/proj/        # Cross-platform core
│   │   ├── proj.cljc                   # Public API + dispatch
│   │   ├── wasm.cljc                   # WASM interface (GraalVM + CLJS workers)
│   │   ├── spec.cljc                   # clojure.spec definitions
│   │   ├── fndefs.cljc                 # PROJ function definitions
│   │   ├── macros.clj                  # JVM macros
│   │   ├── macros.cljs                 # ClojureScript macros
│   │   ├── proj-loader.mjs             # Main-thread WASM orchestrator
│   │   ├── proj-worker.mjs             # Worker for browser/Node.js
│   │   ├── fetch-worker.mjs            # Node.js sync HTTP bridge worker
│   │   ├── *.mjs                       # Cherry-generated JS modules
│   │   └── dist/                       # esbuild bundle output (npm package)
│   └── java/net/willcohen/proj/
│       └── PROJ.java                   # Java API wrapper
├── resources/
│   ├── {platform}/                     # Native libraries per platform
│   ├── wasm/                           # WASM artifacts (GraalVM single-threaded build)
│   │   ├── proj-emscripten.js          # WASM JS glue
│   │   ├── proj-emscripten.wasm        # WASM binary
│   │   └── proj-loader.mjs             # proj-loader.mjs for classpath
│   ├── proj.db                         # PROJ database
│   └── proj.ini                        # PROJ configuration
├── deps.edn                            # Clojure dependencies
├── bb.edn                              # Babashka build tasks
├── build.clj                           # Clojure build configuration
└── flake.nix                           # Nix development environment

Key Implementation Files

Core API & Dispatch:

  • src/cljc/net/willcohen/proj/proj.cljc - Main public API and dispatch logic
  • src/cljc/net/willcohen/proj/fndefs.cljc - PROJ function definitions and constants
  • src/cljc/net/willcohen/proj/macros.clj[s] - Code generation macros for multi-platform support
  • src/cljc/net/willcohen/proj/wasm.cljc - GraalVM context management (CLJ) and worker pool (CLJS)

JVM Implementations:

  • src/clj/net/willcohen/proj/impl/native.clj - JNA/FFI implementation for native libraries
  • src/clj/net/willcohen/proj/impl/struct.clj - Native struct definitions for FFI
  • src/clj/net/willcohen/proj/impl/logging.clj - JNA callback for PROJ log routing
  • src/clj/net/willcohen/proj/impl/network.clj - GraalVM WASM grid fetching callbacks
  • src/c/proj_network_stubs.c - C stubs bridging PROJ's network API to GraalVM Java callbacks

JavaScript Workers:

  • src/cljc/net/willcohen/proj/proj-loader.mjs - Main-thread orchestrator (init, worker pool)
  • src/cljc/net/willcohen/proj/proj-worker.mjs - PROJ worker (browser Web Workers / Node.js worker_threads)
  • src/cljc/net/willcohen/proj/fetch-worker.mjs - Node.js sync HTTP bridge via SharedArrayBuffer + Atomics

Java API:

  • src/java/net/willcohen/proj/PROJ.java - Java wrapper class

Call Flow

proj.cljc is the public API for all platforms. On the JVM, init! tries native FFI first and falls back to GraalVM WASM. On ClojureScript, it initializes the worker pool. After init, all calls dispatch through the active backend.

JVM — Native FFI (preferred):

  proj.cljc         Public API. Blocking calls. Dispatches based on
    │                @implementation (:ffi or :graal).
    ▼
  native.clj        JNA/FFI via dtype-next. Extracts platform-specific
    │                shared libraries from resources/{platform}/ to a temp
    │                dir, loads them via JNA. Direct C calls, no WASM.
    │
    ├─ logging.clj   JNA callback bridging PROJ's log output to
    │                 clojure.tools.logging.
    │
    └─ struct.clj    Native struct definitions (PJ_COORD, etc.) for
                     zero-copy memory access via dtype-next tensors.
                     Grid fetching handled by JNA callbacks to Java HttpClient.
JVM — GraalVM WASM (fallback, or forced via force-graal!):

  proj.cljc         Same public API, same dispatch.
    │
    ▼
  wasm.cljc         Creates a GraalVM polyglot context with JS + WASM support.
    │                Loads proj-emscripten.wasm, proj.db, and proj.ini from
    │                the classpath (resources/wasm/).
    ▼
  proj-loader.mjs   initialize() runs the Emscripten module directly in
    │                the polyglot context. No workers, no postMessage --
    │                JVM ↔ JS interop is via ProxyArray/ProxyObject.
    │
    ├─ network.clj   Grid fetching callbacks. C stubs (proj_network_stubs.c)
    │                 in the WASM binary call into Java via globalThis, where
    │                 network.clj handles HTTP with Java's HttpClient.
    │
    └─ logging.clj   Same log routing as FFI, adapted for GraalVM callbacks.
Browser / Node.js (ClojureScript):

  proj.cljc         Public API. All operations return Promises.
    │
    ▼
  wasm.cljc         Worker pool management. Routes each call to the
    │                worker that owns the relevant PJ context.
    ▼
  proj-loader.mjs   Main thread. Spawns workers, loads resources (proj.db,
    │                proj.ini, WASM binary), and manages a promise-per-call
    │                protocol: each outgoing postMessage gets a unique ID,
    │                and the response resolves the matching Promise.
    ▼
  proj-worker.mjs   Runs in a Web Worker (browser) or worker_thread (Node.js).
    │                All PROJ ccall/malloc/free operations happen here.
    │                Loads the Emscripten module and writes proj.db/proj.ini
    │                to Emscripten's virtual filesystem.
    ▼
  fetch-worker.mjs  Node.js only. A second worker_thread that bridges
                     Emscripten's synchronous XMLHttpRequest (used for grid
                     fetching) to Node.js async http/https via SharedArrayBuffer
                     + Atomics.wait/notify. Browsers don't need this because
                     Web Workers can use Emscripten's built-in FETCH support.

WASM Build Variants

The bb build --wasm task produces two WASM builds from the same PROJ source:

  • Pthreads build (for ClojureScript browser/Node.js) -- output goes to src/cljc/net/willcohen/proj/ alongside the source, where the cherry/esbuild pipeline bundles it into dist/.
  • Single-threaded build (for GraalVM) -- output goes to resources/wasm/ on the classpath. GraalVM's polyglot engine doesn't support pthreads, so it needs this variant.

Both builds produce files named proj-emscripten.js and proj-emscripten.wasm; they live in different directories.

Performance Considerations

  • Initialization: Native FFI is near-instant, GraalVM takes 5-7 seconds
  • Transformations: Native is fastest, followed by direct WASM, then GraalVM
  • Memory: Coordinate arrays use platform-specific optimizations

Development

1. Nix Flake and Direnv

To avoid an ever evolving set of dependencies where specific versions can cause errors with the build process, a nix flake has been provided that will work with direnv. (See .envrc's use flake).

This should allow for a development environment that is consistent and known to work.

2. Local REPL

Development REPLs with different configurations:

# Rich development REPL with Portal and other tools
bb dev

# Basic nREPL with Portal (port 7888)
bb nrepl

# Standard Clojure REPL
clj

It may be helpful to use an editor with Clojure functionality: Emacs with CIDER, VSCode with Calva and Portal extensions, IDEA and Cursive.

The bb nrepl task starts an nREPL server on port 7888 with Portal included for data visualization and debugging.

3. Demo Server

Run the browser demo locally:

bb demo  # Serves at http://localhost:8080/docs/

Navigate to http://localhost:8080/docs/ to see the library in action.

4. Documentation

Generate API documentation (work in progress):

bb quickdoc  # Generates docs from source

5. Task Reference

Run bb tasks for complete list. Key commands:

Build & Package:

  • bb build --help - Show build options (native/wasm/cross)
  • bb build:all - Build native + WASM + cross-platform artifacts
  • bb jar - Build JAR file for JVM
  • bb cherry - Build JavaScript ES6 module
  • bb pom - Generate/update pom.xml

Testing:

  • bb test:all - Run all tests
  • bb test:ffi / bb test:node / bb test:graal - Test specific implementations
  • bb test-run - Complete build + test cycle
  • bb pre-deploy - Full build, test, and package verification before deploy

Development:

  • bb dev - Rich REPL with Portal
  • bb nrepl - nREPL server (port 7888)
  • bb demo - Browser demo (localhost:8080)

Deployment:

  • bb deploy:dry-run - Check auth, show JAR/npm contents, npm publish dry-run
  • bb deploy - Tag, push, deploy to Clojars + npm
  • bb version-bump <version> - Bump version across all files

CI:

  • bb download-ci-artifacts - Download all build artifacts (source archives, native libs, WASM) from the most recent CI run (requires gh CLI)

Utilities:

  • bb clean --help - Show clean options
  • bb jar-contents - List files in JAR
  • bb npm-contents - List files in npm package
  • bb proj:clone --help - Local PROJ development

7. Local PROJ Development Workflow

For developers working on the PROJ C library itself, clj-proj provides a workflow to test local PROJ changes against the bindings before submitting upstream.

Setup:

# Clone PROJ repository locally
bb proj:clone                    # Clone to vendor/PROJ (master branch)
bb proj:clone --branch=feature   # Clone specific branch
bb proj:clone --update           # Update existing clone

Development Workflow:

# Make changes to PROJ C code
cd vendor/PROJ
# ... edit C files, add features, fix bugs ...
git commit -m "experimental change"
cd ../..

# Test changes against clj-proj
bb build --native --local-proj --debug    # Use local PROJ instead of release
bb test:ffi                                # Verify bindings still work

# Test WASM compatibility
bb build --wasm --local-proj
bb test:node

# Cross-platform verification
bb build --cross --local-proj             # Test musl builds with local PROJ

Local PROJ Tasks:

  • proj:clone - Clone OSGeo/PROJ repository with options (--help for details)

Local PROJ Build Flags:

  • --local-proj - Use vendor/PROJ instead of released PROJ version (works with any build task)

Directory Structure:

clj-proj/
├── vendor/           # gitignored - your local development area
│   └── PROJ/         # cloned OSGeo/PROJ repository
├── bb.edn
└── ...

This workflow enables integration testing between upstream PROJ C library development and clj-proj bindings, catching upstream API changes and build proces.

License

Copyright (c) 2024, 2025, 2026 Will Cohen

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

--

This project uses code from PROJ, which is distributed under the following terms:

All source, data files and other contents of the PROJ package are 
available under the following terms.  Note that the PROJ 4.3 and earlier
was "public domain" as is common with US government work, but apparently
this is not a well defined legal term in many countries. Frank Warmerdam placed
everything under the following MIT style license because he believed it is
effectively the same as public domain, allowing anyone to use the code as
they wish, including making proprietary derivatives.

Initial PROJ 4.3 public domain code was put as Frank Warmerdam as copyright
holder, but he didn't mean to imply he did the work. Essentially all work was
done by Gerald Evenden.

Copyright information can be found in source files.

 --------------

 Permission is hereby granted, free of charge, to any person obtaining a
 copy of this software and associated documentation files (the "Software"),
 to deal in the Software without restriction, including without limitation
 the rights to use, copy, modify, merge, publish, distribute, sublicense,
 and/or sell copies of the Software, and to permit persons to whom the
 Software is furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included
 in all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 DEALINGS IN THE SOFTWARE.

--

This project (in particular the implemented web interface) uses code from wasm-proj, which is distributed under the following terms:

MIT License

Copyright (c) 2025 Javier Jimenez Shaw

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

--

This project uses code from libtiff, which distributed under the following terms:

Copyright © 1988-1997 Sam Leffler
Copyright © 1991-1997 Silicon Graphics, Inc.

Permission to use, copy, modify, distribute, and sell this software and 
its documentation for any purpose is hereby granted without fee, provided
that (i) the above copyright notices and this permission notice appear in
all copies of the software and related documentation, and (ii) the names of
Sam Leffler and Silicon Graphics may not be used in any advertising or
publicity relating to the software without the specific, prior written
permission of Sam Leffler and Silicon Graphics.

THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.

IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF 
LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 
OF THIS SOFTWARE.

--

This project bundles SQLite, which is in the public domain. See SQLite Copyright for details.

--

This project uses zlib, which is distributed under the following terms:

Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler

This software is provided 'as-is', without any express or implied
warranty.  In no event will the authors be held liable for any damages
arising from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:

1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software
   in a product, an acknowledgment in the product documentation would be
   appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

--

The browser demo uses coi-serviceworker for SharedArrayBuffer support on static hosting, which is distributed under the MIT license:

MIT License

Copyright (c) 2021 Guido Zuidhof

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

--

This project statically links musl libc for Linux builds, which is distributed under the following terms:

Copyright © 2005-2020 Rich Felker, et al.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

--

This project statically links MinGW-w64 runtime libraries for Windows builds. The MinGW-w64 runtime is distributed under various permissive licenses:

MinGW-w64 runtime licensing
***************************

This program or library was built using MinGW-w64 and statically
linked against the MinGW-w64 runtime. Some parts of the runtime
are under licenses which require that the copyright and license
notices are included when distributing the code in binary form.
These notices are listed below.


========================
Overall copyright notice
========================

Copyright (c) 2009, 2010, 2011, 2012, 2013 by the mingw-w64 project

This license has been certified as open source. It has also been designated
as GPL compatible by the Free Software Foundation (FSF).

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

   1. Redistributions in source code must retain the accompanying copyright
      notice, this list of conditions, and the following disclaimer.
   2. Redistributions in binary form must reproduce the accompanying
      copyright notice, this list of conditions, and the following disclaimer
      in the documentation and/or other materials provided with the
      distribution.
   3. Names of the copyright holders must not be used to endorse or promote
      products derived from this software without prior written permission
      from the copyright holders.
   4. The right to distribute this software or to use it for any purpose does
      not give you the right to use Servicemarks (sm) or Trademarks (tm) of
      the copyright holders.  Use of them is covered by separate agreement
      with the copyright holders.
   5. If any files are modified, you must cause the modified files to carry
      prominent notices stating that you changed the files and the date of
      any change.

Disclaimer

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

See MinGW-w64 runtime licensing

--

Data Files

This project includes PROJ data files (proj.db, proj.ini) which contain coordinate system definitions from various sources including EPSG. These are distributed under the same terms as PROJ itself (MIT/X11 style license).

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close