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
322 changes: 119 additions & 203 deletions HOST_FUNCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,240 +1,166 @@
# Host Functions

The flagd-evaluator WASM module requires the host environment to provide certain functions. These are imported by the WASM module and must be implemented by the runtime (e.g., Java/Chicory, JavaScript, Go, etc.).
The flagd-evaluator WASM module requires the host environment to provide certain functions. These are imported by the WASM module and must be implemented by the runtime (e.g., Java/Chicory, Go/wazero, JavaScript).

## Required Host Function: `get_current_time_unix_seconds`
## Import Overview

**Module:** `host`
**Function name:** `get_current_time_unix_seconds`
**Signature:** `() -> u64`
The WASM module declares imports across 3 modules:

### Purpose
| Module | Functions | Stability |
|--------|-----------|-----------|
| `host` | 1 (stable names) | Stable — names never change |
| `__wbindgen_placeholder__` | ~6 (hashed names) | Names change with Rust dependency updates |
| `__wbindgen_externref_xform__` | ~2 (fixed names) | Names are stable but may appear/disappear |

Provides the current Unix timestamp (seconds since epoch: 1970-01-01 00:00:00 UTC) to the WASM module. This is used for context enrichment to populate the `$flagd.timestamp` property, which can be used in targeting rules.
**Important:** The `__wbindgen_placeholder__` function names include a hash suffix (e.g., `__wbg_getTime_ad1e9878a735af08`) that changes whenever Rust dependencies or wasm-bindgen versions change. Host implementations should **match by prefix**, not by exact name. See the [Dynamic Matching](#dynamic-matching-recommended) section.

### Why a Host Function?
## Stable Host Functions

The WASM sandbox cannot access system time directly without WASI support. Since Chicory and other pure WASM runtimes don't provide WASI, the host must supply the current time.
### `host::get_current_time_unix_seconds`

### Return Value
**Signature:** `() -> i64`

- **Type:** `u64`
- **Value:** Unix timestamp in seconds since epoch
- **Example:** `1735689600` (represents 2025-01-01 00:00:00 UTC)
Provides the current Unix timestamp (seconds since epoch) for `$flagd.timestamp` context enrichment. The WASM sandbox cannot access system time without WASI, so the host must supply it.

## Implementation Examples
**Return value:** Unix timestamp in seconds (e.g., `1735689600` for 2025-01-01 00:00:00 UTC).

### Java (Chicory)
**If not provided:** The module defaults `$flagd.timestamp` to `0`. Time-based targeting won't work, but evaluation continues without errors.

```java
import com.dylibso.chicory.runtime.HostFunction;
import com.dylibso.chicory.wasm.types.Value;
import com.dylibso.chicory.wasm.types.ValueType;

// Create the host function
HostFunction getCurrentTime = new HostFunction(
"host", // Module name
"get_current_time_unix_seconds", // Function name
List.of(), // No parameters
List.of(ValueType.I64), // Returns i64
(Instance instance, Value... args) -> {
long currentTimeSeconds = System.currentTimeMillis() / 1000;
return new Value[] { Value.i64(currentTimeSeconds) };
}
);
## wasm-bindgen Functions

// Add to module imports when loading WASM
Module module = Module.builder(wasmBytes)
.withHostFunction(getCurrentTime)
.build();
```
These imports come from Rust dependencies (chrono, getrandom) using wasm-bindgen. Their names contain hashes that change across builds. Match by prefix.

### Complete Java Example
### `__wbg_getRandomValues_*`

```java
import com.dylibso.chicory.runtime.*;
import com.dylibso.chicory.wasm.types.*;
import java.nio.charset.StandardCharsets;

public class FlagdEvaluatorWithHostFunctions {
public static void main(String[] args) {
// Load WASM module
byte[] wasmBytes = Files.readAllBytes(Path.of("flagd_evaluator.wasm"));

// Define host function for current time
HostFunction getCurrentTime = new HostFunction(
"host",
"get_current_time_unix_seconds",
List.of(),
List.of(ValueType.I64),
(Instance instance, Value... unused) -> {
long now = System.currentTimeMillis() / 1000;
return new Value[] { Value.i64(now) };
}
);

// Build module with host function
Module module = Module.builder(wasmBytes)
.withHostFunction(getCurrentTime)
.build();

Instance instance = module.instantiate();

// Get WASM exports
ExportFunction alloc = instance.export("alloc");
ExportFunction dealloc = instance.export("dealloc");
ExportFunction updateState = instance.export("update_state");
ExportFunction evaluate = instance.export("evaluate");

// Load flag configuration
String config = """
{
"flags": {
"time-based-flag": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "off",
"targeting": {
"if": [
{">": [{"var": "$flagd.timestamp"}, 1700000000]},
"on",
"off"
]
}
}
}
}
""";
**Module:** `__wbindgen_placeholder__`
**Signature:** `(i32, i32) -> void`
**Purpose:** Cryptographic entropy for hash table seeding (ahash in boon JSON schema validation).

// Update state
byte[] configBytes = config.getBytes(StandardCharsets.UTF_8);
int configPtr = alloc.apply(Value.i32(configBytes.length))[0].asInt();
instance.memory().write(configPtr, configBytes);
The first argument is an externref index (can be ignored). The second argument is a pointer to a 32-byte buffer in WASM memory. Fill the buffer with random bytes.

long packedResult = updateState.apply(
Value.i32(configPtr),
Value.i32(configBytes.length)
)[0].asLong();
### `__wbg_new_0_*`

dealloc.apply(Value.i32(configPtr), Value.i32(configBytes.length));
**Module:** `__wbindgen_placeholder__`
**Signature:** `() -> i32`
**Purpose:** JavaScript `Date` constructor shim (used by chrono's wasmbind feature).

// Evaluate flag with context
String flagKey = "time-based-flag";
String context = "{\"email\":\"user@example.com\"}";
Return `0` (dummy reference). The actual timestamp is provided by `host::get_current_time_unix_seconds`.

byte[] flagKeyBytes = flagKey.getBytes(StandardCharsets.UTF_8);
byte[] contextBytes = context.getBytes(StandardCharsets.UTF_8);
### `__wbg_getTime_*`

int flagKeyPtr = alloc.apply(Value.i32(flagKeyBytes.length))[0].asInt();
int contextPtr = alloc.apply(Value.i32(contextBytes.length))[0].asInt();
**Module:** `__wbindgen_placeholder__`
**Signature:** `(i32) -> f64`
**Purpose:** JavaScript `Date.getTime()` shim.

instance.memory().write(flagKeyPtr, flagKeyBytes);
instance.memory().write(contextPtr, contextBytes);
Return current time in **milliseconds** as f64. The argument is the Date reference from `new_0` (ignored).

long evalResult = evaluate.apply(
Value.i32(flagKeyPtr),
Value.i32(flagKeyBytes.length),
Value.i32(contextPtr),
Value.i32(contextBytes.length)
)[0].asLong();
### `__wbg___wbindgen_throw_*`

// Unpack result pointer and length
int resultPtr = (int) (evalResult >> 32);
int resultLen = (int) (evalResult & 0xFFFFFFFF);
**Module:** `__wbindgen_placeholder__`
**Signature:** `(i32, i32) -> void`
**Purpose:** Error propagation from WASM to host.

byte[] resultBytes = new byte[resultLen];
instance.memory().read(resultPtr, resultBytes);
String result = new String(resultBytes, StandardCharsets.UTF_8);
Arguments are (pointer, length) of a UTF-8 error message in WASM memory. The host should throw/raise an exception with the message.

System.out.println("Evaluation result: " + result);
### `__wbindgen_object_drop_ref`, `__wbindgen_describe`

// Clean up
dealloc.apply(Value.i32(flagKeyPtr), Value.i32(flagKeyBytes.length));
dealloc.apply(Value.i32(contextPtr), Value.i32(contextBytes.length));
dealloc.apply(Value.i32(resultPtr), Value.i32(resultLen));
}
}
```
**Module:** `__wbindgen_placeholder__`
**Signature:** `(i32) -> void`
**Purpose:** wasm-bindgen internals. No-ops — these track JavaScript object references which don't exist in non-browser runtimes.

### JavaScript (Node.js with WASI)
### `__wbindgen_externref_table_grow`

```javascript
const fs = require('fs');
**Module:** `__wbindgen_externref_xform__`
**Signature:** `(i32) -> i32`
**Purpose:** Grow the external reference table. Return a fixed value (e.g., `128`).

// Load WASM
const wasmBytes = fs.readFileSync('flagd_evaluator.wasm');
### `__wbindgen_externref_table_set_null`

// Define host functions
const importObject = {
host: {
get_current_time_unix_seconds: () => {
return BigInt(Math.floor(Date.now() / 1000));
}
}
};
**Module:** `__wbindgen_externref_xform__`
**Signature:** `(i32) -> void`
**Purpose:** Set externref table entry to null. No-op.

## Dynamic Matching (Recommended)

Instead of hardcoding exact function names, inspect the WASM module's import section at startup and match by prefix. This way, hash changes from Rust dependency updates don't require host code changes.

// Instantiate WASM with host functions
WebAssembly.instantiate(wasmBytes, importObject)
.then(({ instance }) => {
// Use the WASM exports
const { alloc, dealloc, evaluate } = instance.exports;
// ... rest of implementation
### Java (Chicory)

```java
WasmModule module = CompiledEvaluator.load();
Store store = new Store();

module.importSection().stream()
.filter(FunctionImport.class::isInstance)
.map(FunctionImport.class::cast)
.forEach(fi -> {
String mod = fi.module();
String name = fi.name();

if ("host".equals(mod) && "get_current_time_unix_seconds".equals(name)) {
// Register timestamp provider
} else if (name.startsWith("__wbg_getRandomValues_")) {
// Register random bytes provider
} else if (name.startsWith("__wbg_getTime_")) {
// Register Date.getTime shim
} else if (name.startsWith("__wbg_new_0_")) {
// Register Date constructor shim (return 0)
} else if (name.contains("__wbindgen_throw")) {
// Register throw handler
} else {
// Register no-op for all other wasm-bindgen imports
}
});
```

### Go
See `WasmRuntime.java` for the full implementation.

### Go (wazero)

```go
package main

import (
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)

func main() {
ctx := context.Background()
runtime := wazero.NewRuntime(ctx)
defer runtime.Close(ctx)

// Define host function
hostModule := runtime.NewHostModuleBuilder("host")
hostModule.NewFunctionBuilder().
WithFunc(func() int64 {
return time.Now().Unix()
}).
Export("get_current_time_unix_seconds")

_, err := hostModule.Instantiate(ctx)
if err != nil {
panic(err)
// Match by prefix when registering host functions
hostBuilder := r.NewHostModuleBuilder("__wbindgen_placeholder__")

// Use the actual import names from the WASM binary
for _, imp := range wasmModule.ImportedFunctions() {
moduleName, name, _ := imp.Import()
switch {
case strings.HasPrefix(name, "__wbg_getRandomValues_"):
hostBuilder.NewFunctionBuilder().
WithFunc(func(ctx context.Context, mod api.Module, externref, bufPtr uint32) { ... }).
Export(name)
// ... other prefix matches
}

// Load and instantiate WASM module
wasmBytes, _ := os.ReadFile("flagd_evaluator.wasm")
module, _ := runtime.Instantiate(ctx, wasmBytes)

// Use the WASM exports
// ...
}
```

## Behavior Without Host Function
### JavaScript

If the host function is not provided:
- The WASM module will catch the panic and default `$flagd.timestamp` to `0`
- This allows targeting rules to detect unavailable time by checking for `timestamp == 0`
- Evaluation will continue without errors, but time-based targeting will not work correctly
```javascript
// In JavaScript, wasm-bindgen host functions are usually provided
// automatically by the generated JS glue code. For manual usage:
const importObject = {
host: {
get_current_time_unix_seconds: () => BigInt(Math.floor(Date.now() / 1000))
},
__wbindgen_placeholder__: new Proxy({}, {
get: (target, name) => {
// Dynamic proxy handles any wbindgen function name
if (name.startsWith('__wbg_getRandomValues_'))
return (ref, ptr) => { crypto.getRandomValues(new Uint8Array(memory.buffer, ptr, 32)); };
if (name.startsWith('__wbg_getTime_'))
return (ref) => Date.now();
// ... etc
return () => {}; // no-op fallback
}
})
};
```

## Testing the Host Function
## Testing

You can verify the host function is working by:
Verify host functions work by evaluating a flag with time-based targeting:

1. Create a flag with time-based targeting:
```json
{
"flags": {
Expand All @@ -243,21 +169,11 @@ You can verify the host function is working by:
"variants": {"on": true, "off": false},
"defaultVariant": "off",
"targeting": {
"if": [
{">": [{"var": "$flagd.timestamp"}, 0]},
"on",
"off"
]
"if": [{">": [{"var": "$flagd.timestamp"}, 0]}, "on", "off"]
}
}
}
}
```

2. Evaluate the flag and check that `$flagd.timestamp` is non-zero in the context

3. The flag should resolve to `"on"` if the timestamp is provided correctly

## Future Host Functions

This document will be updated as additional host functions are added to the WASM module.
The flag should resolve to `"on"` when the timestamp host function is provided correctly.
Loading
Loading