emscripten-core / emscripten-core/emscripten
cwrap with returnType "boolean" returns a number when all args are numeric
- 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
Assessment
This issue has not been assessed yet.