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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ jobs:
done
exit 1

# a decimal-comma locale for c-locale-test
- name: Install de_DE.UTF-8
if: runner.os == 'Linux'
run: sudo locale-gen de_DE.UTF-8

# Runs the suite through babashka's built-in babashka.ffi, so the calls
# go through the compiled trampolines instead of FFM handles.
- name: Run tests
Expand Down Expand Up @@ -85,6 +90,11 @@ jobs:
with:
cli: latest

# a decimal-comma locale for c-locale-test
- name: Install de_DE.UTF-8
if: runner.os == 'Linux'
run: sudo locale-gen de_DE.UTF-8

- name: Run tests
run: clojure -M:test

Expand Down
36 changes: 36 additions & 0 deletions doc/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Without either setting, the JDK warns about native access.
- [Unions](#unions)
- [Out parameters](#out-parameters)
- [Create a callback](#create-a-callback)
- [Set the C locale](#set-the-c-locale)
- [Performance and limits](#performance-and-limits)
- [On the JVM](#on-the-jvm)
- [In a babashka native binary](#in-a-babashka-native-binary)
Expand Down Expand Up @@ -946,6 +947,41 @@ A `:bool` callback argument becomes `true` or `false`.
> **_NOTE_** Do not let a callback throw an exception. Catch exceptions inside
> the callback, or the process can stop.

## Set the C locale

Set `LC_NUMERIC` to `"C"` before you call C code that parses or prints
floating-point numbers. On Linux and macOS, the JVM and babashka take the C
locale from the environment. On Windows, the C locale stays `"C"`.

In a locale with a decimal comma, such as `de_DE.UTF-8`, every C function that
parses or formats a floating-point number expects or writes a comma, and none of
them reports an error. For example, `strtod` reads `"0.95"` as `0.0`, so a
library that parses a JSON or glTF file gets wrong values. And `printf("%g",
0.95)` writes `0,95`, so a CSV or JSON file that was written by C, no longer
parses elsewhere.

To fix this, on Linux and macOS, call `setlocale`:

```clojure
;; the value of LC_NUMERIC in locale.h: 1 on Linux, 4 on macOS
(def lc-numeric
(if (re-find #"(?i)linux" (System/getProperty "os.name")) 1 4))

(let [setlocale (ffi/cfn "setlocale" [:int :string] :pointer)]
(setlocale lc-numeric "C"))
```

Alternatively, set `LC_NUMERIC=C` in the environment before starting babashka.
`LC_ALL` overrides `LC_NUMERIC`.

```sh
LC_NUMERIC=C bb script.clj
```

Unfortunately babashka.ffi cannot do this for you. The locale is shared by the
whole process, and babashka.ffi cannot tell whether other code set it on
purpose.

## Performance and limits

The host determines how `babashka.ffi` calls C and how much each call costs.
Expand Down
30 changes: 30 additions & 0 deletions test/babashka/ffi_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -958,3 +958,33 @@
(is (= (:babashka.ffi/backend (meta c-abs))
(:babashka.ffi/backend (meta (vary-meta c-abs assoc :x 1)))))
(is (= 5 ((vary-meta c-abs assoc :x 1) -5)))))))

(deftest c-locale-test
(if-not @default-lookup?
(println "C locale skipped: this build has no default lookup")
(let [os (System/getProperty "os.name")
windows? (str/starts-with? os "Windows")
_ (when windows? (ffi/load-library "ucrtbase.dll"))
lc-numeric (if (re-find #"(?i)linux" os) 1 4)
query (ffi/cfn "setlocale" [:int :pointer] :pointer)
setlocale (ffi/cfn "setlocale" [:int :string] :pointer)
strtod (ffi/cfn "strtod" [:string :pointer] :double)
current #(ffi/ptr->string (query lc-numeric ffi/null))]
(if windows?
(testing "LC_NUMERIC is \"C\" on Windows and strtod reads \"0.95\" as 0.95"
(is (= "C" (current)))
(is (= 0.95 (strtod "0.95" ffi/null))))
(let [env (some #(not-empty (System/getenv %)) ["LC_ALL" "LC_NUMERIC" "LANG"])
original (current)]
(testing "LC_NUMERIC equals LC_ALL, LC_NUMERIC or LANG from the environment"
(is (= (or env "C") original)))
(if (ffi/null? (setlocale lc-numeric "de_DE.UTF-8"))
(println "decimal comma skipped: de_DE.UTF-8 is not installed")
(try
(testing "strtod reads \"0.95\" as 0.0 in de_DE.UTF-8"
(is (= 0.0 (strtod "0.95" ffi/null))))
(testing "setlocale LC_NUMERIC \"C\" makes strtod read \"0.95\" as 0.95"
(setlocale lc-numeric "C")
(is (= 0.95 (strtod "0.95" ffi/null))))
(finally
(setlocale lc-numeric original)))))))))
Loading