emscripten-core / emscripten-core/emscripten

cwrap with returnType "boolean" returns a number when all args are numeric

Open
#27,021 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
27.6k
Forks
3.6k
Avg merge
1d 1h
Merged PRs (30d)
105

Description

## Summary

`cwrap(ident, "boolean", argTypes)` returns a JS `number` (0 or 1) instead of a `boolean` when `argTypes` consists only of `"number"` / `"boolean"`. This is inconsistent with `ccall(ident, "boolean", argTypes, args)`, which correctly returns a JS `boolean`. The `cwrap` docs list `"boolean"` as a valid return type, so the fast-path skip is a silent contract violation.

## Cause

In `src/library_ccall.js`, `cwrap`'s fast-path test treats `"boolean"` returns as numeric:

```js
var cwrap = (ident, returnType, argTypes, opts) => {
var numericArgs = !argTypes || argTypes.every((type) => type === "number" || type === "boolean");
var numericRet = returnType !== "string"; // ← "boolean" counted as numeric
if (numericRet && numericArgs && !opts) return getCFunc(ident);
return (...args) => ccall(ident, returnType, argTypes, args, opts);
};
```

When the fast path triggers, `getCFunc(ident)` is the raw WASM export, so `Boolean(ret)` from `ccall`'s `convertReturnValue` is never applied.

The asymmetry traces back to #17511, which extended the fast path to accept `"boolean"` as an *argument* type but didn't update the return-type check correspondingly.

## Reproduction

`repro.c`:
```c
#include

bool some_bool_func(int x) {
return x != 0;
}
```

Build:
```bash
emcc repro.c -o repro.mjs \
-sMODULARIZE=1 -sEXPORT_ES6=1 \
-sENVIRONMENT=node \
-sEXPORTED_FUNCTIONS='["_some_bool_func"]' \
-sEXPORTED_RUNTIME_METHODS='["cwrap","ccall"]'
```

`test.mjs`:
```js
import createModule from "./repro.mjs";

const mod = await createModule();
const viaCwrap = mod.cwrap("some_bool_func", "boolean", ["number"]);

const r = viaCwrap(1);
console.log("cwrap result:", r, "typeof:", typeof r);
console.log("ccall result:", mod.ccall("some_bool_func", "boolean", ["number"], [1]));
```

Run: `node test.mjs`

Actual:
```
cwrap result: 1 typeof: number
ccall result: true
```

Expected:
```
cwrap result: true typeof: boolean
ccall result: true
```

## Impact

For plain-JS callers, `if (result)` truthiness coercion masks the bug. It surfaces for strictly-typed consumers (Scala.js, Kotlin/JS, TypeScript when the `cwrap` return is annotated `boolean`) — the value crosses the JS↔host boundary as a number and the host's type check rejects the cast.

## Possible fixes

1. Exclude `"boolean"` from the fast-path return check: `var numericRet = returnType !== "string" && returnType !== "boolean";` — falls through to `ccall`, which already does `Boolean(ret)`. Smallest diff, correctness wins.
2. Keep the fast path but wrap with a `Boolean()` thunk when `returnType === "boolean"`. Preserves the per-call perf win.
3. Document `"boolean"` as unsupported as a `cwrap` return type (only valid via `ccall`).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.