Make library work with C++20 modules (#4764)

* :white_check_mark: add test for C++20 modules

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* :rotating_light: fix warning

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Add missing header (#4763)

* :bug: add missing header

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* :rotating_light: fix warning

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* :rotating_light: fix warning

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml
index 6defddc..179038f 100644
--- a/.github/workflows/ubuntu.yml
+++ b/.github/workflows/ubuntu.yml
@@ -143,7 +143,7 @@
     strategy:
       matrix:
         # older GCC docker images (4, 5, 6) fail to check out code
-        compiler: ['7', '8', '9', '10', '11', '12', '13', '14', 'latest']
+        compiler: ['7', '8', '9', '10', '11', '12', '13', '14', '15', 'latest']
     container: gcc:${{ matrix.compiler }}
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -221,6 +221,21 @@
       - name: Build
         run: cmake --build build --target ci_cuda_example
 
+  ci_module_cpp20:
+    strategy:
+      matrix:
+        container: ['gcc:latest', 'silkeh/clang:latest']
+    runs-on: ubuntu-latest
+    container: ${{ matrix.container }}
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      - name: Get latest CMake and ninja
+        uses: lukka/get-cmake@57c20a23a6cac5b90f31864439996e5b206df9dc # v4.0.1
+      - name: Run CMake
+        run: cmake -S . -B build -DJSON_CI=On
+      - name: Build
+        run: cmake --build build --target ci_module_cpp20
+
   ci_icpc:
     runs-on: ubuntu-latest
     container: ghcr.io/nlohmann/json-ci:v2.2.0
diff --git a/cmake/ci.cmake b/cmake/ci.cmake
index ef4c257..8c0d210 100644
--- a/cmake/ci.cmake
+++ b/cmake/ci.cmake
@@ -660,6 +660,17 @@
 )
 
 ###############################################################################
+# C++ 20 modules
+###############################################################################
+
+add_custom_target(ci_module_cpp20
+    COMMAND ${CMAKE_COMMAND}
+        -DCMAKE_BUILD_TYPE=Debug -GNinja
+        -S${PROJECT_SOURCE_DIR}/tests/module_cpp20 -B${PROJECT_BINARY_DIR}/ci_module_cpp20
+    COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/ci_module_cpp20
+)
+
+###############################################################################
 # Intel C++ Compiler
 ###############################################################################
 
diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp
index 92b8557..ed0ee5b 100644
--- a/include/nlohmann/detail/input/binary_reader.hpp
+++ b/include/nlohmann/detail/input/binary_reader.hpp
@@ -53,7 +53,7 @@
 
 @note from https://stackoverflow.com/a/1001328/266378
 */
-static inline bool little_endianness(int num = 1) noexcept
+inline bool little_endianness(int num = 1) noexcept
 {
     return *reinterpret_cast<char*>(&num) == 1;
 }
diff --git a/include/nlohmann/detail/string_escape.hpp b/include/nlohmann/detail/string_escape.hpp
index 7f02318..d969a9e 100644
--- a/include/nlohmann/detail/string_escape.hpp
+++ b/include/nlohmann/detail/string_escape.hpp
@@ -62,7 +62,7 @@
  * Note the order of escaping "~1" to "/" and "~0" to "~" is important.
  */
 template<typename StringType>
-static void unescape(StringType& s)
+inline void unescape(StringType& s)
 {
     replace_substring(s, StringType{"~1"}, StringType{"/"});
     replace_substring(s, StringType{"~0"}, StringType{"~"});
diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp
index dabad06..850cd8e 100644
--- a/single_include/nlohmann/json.hpp
+++ b/single_include/nlohmann/json.hpp
@@ -3126,7 +3126,7 @@
  * Note the order of escaping "~1" to "/" and "~0" to "~" is important.
  */
 template<typename StringType>
-static void unescape(StringType& s)
+inline void unescape(StringType& s)
 {
     replace_substring(s, StringType{"~1"}, StringType{"/"});
     replace_substring(s, StringType{"~0"}, StringType{"~"});
@@ -9868,7 +9868,7 @@
 
 @note from https://stackoverflow.com/a/1001328/266378
 */
-static inline bool little_endianness(int num = 1) noexcept
+inline bool little_endianness(int num = 1) noexcept
 {
     return *reinterpret_cast<char*>(&num) == 1;
 }
diff --git a/tests/module_cpp20/CMakeLists.txt b/tests/module_cpp20/CMakeLists.txt
new file mode 100644
index 0000000..2388434
--- /dev/null
+++ b/tests/module_cpp20/CMakeLists.txt
@@ -0,0 +1,12 @@
+cmake_minimum_required(VERSION 3.28)
+
+project(json_test CXX)
+
+add_executable(json_test)
+
+target_sources(json_test
+    PRIVATE main.cpp
+    PUBLIC FILE_SET cxx_modules TYPE CXX_MODULES FILES json.cpp)
+
+target_compile_features(json_test PUBLIC cxx_std_20)
+target_include_directories(json_test PRIVATE ../../include)
diff --git a/tests/module_cpp20/json.cpp b/tests/module_cpp20/json.cpp
new file mode 100644
index 0000000..8d5cc91
--- /dev/null
+++ b/tests/module_cpp20/json.cpp
@@ -0,0 +1,17 @@
+module;
+#include <nlohmann/json.hpp>
+export module json;
+
+export namespace nlohmann
+{
+using ::nlohmann::adl_serializer;
+
+using ::nlohmann::basic_json;
+using ::nlohmann::json_pointer;
+
+using ::nlohmann::json;
+using ::nlohmann::ordered_json;
+using ::nlohmann::ordered_map;
+
+using ::nlohmann::json_pointer;
+}  // namespace nlohmann
diff --git a/tests/module_cpp20/main.cpp b/tests/module_cpp20/main.cpp
new file mode 100644
index 0000000..ad7bd8b
--- /dev/null
+++ b/tests/module_cpp20/main.cpp
@@ -0,0 +1,6 @@
+import json;
+
+int main()
+{
+    nlohmann::json j;
+}