From 1942e0ef0171711b5a157ea29ec6dddc079df5be Mon Sep 17 00:00:00 2001 From: Miko <110693261+mikomikotaishi@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:27:09 -0500 Subject: [PATCH] Add C++ modules support (#2291) * Add C++ modules support * Add module examples * Missing semicolon * Update GitHub Actions script and create a modules updating script * Name the unused param * Use the guarded/direct export of header approach * Update CMakeLists.txt Co-authored-by: Andrea Pappacoda * Update CMakeLists.txt Co-authored-by: Andrea Pappacoda * Split scripts into split.py and generate_module.py --------- Co-authored-by: Andrea Pappacoda --- CMakeLists.txt | 51 ++++++++++++++++++++++++++++-- cmake/modules.cmake | 16 ++++++++++ generate_module.py | 77 +++++++++++++++++++++++++++++++++++++++++++++ split.py | 26 ++++++++------- 4 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 cmake/modules.cmake create mode 100644 generate_module.py diff --git a/CMakeLists.txt b/CMakeLists.txt index e51611e..45a0e16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ * HTTPLIB_USE_ZLIB_IF_AVAILABLE (default on) * HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on) * HTTPLIB_USE_ZSTD_IF_AVAILABLE (default on) + * HTTPLIB_BUILD_MODULES (default off) * HTTPLIB_REQUIRE_OPENSSL (default off) * HTTPLIB_REQUIRE_ZLIB (default off) * HTTPLIB_REQUIRE_BROTLI (default off) @@ -110,6 +111,15 @@ option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system cer option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON) option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF) option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON) +# C++20 modules support requires CMake 3.28 or later +if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28") + option(HTTPLIB_BUILD_MODULES "Build httplib modules (requires HTTPLIB_COMPILE to be ON)." OFF) +else() + set(HTTPLIB_BUILD_MODULES OFF CACHE INTERNAL "Build httplib modules disabled (requires CMake 3.28+)" FORCE) + if(DEFINED CACHE{HTTPLIB_BUILD_MODULES} AND HTTPLIB_BUILD_MODULES) + message(WARNING "HTTPLIB_BUILD_MODULES requires CMake 3.28 or later. Current version is ${CMAKE_VERSION}. Modules support has been disabled.") + endif() +endif() # Defaults to static library but respects standard BUILD_SHARED_LIBS if set include(CMakeDependentOption) cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only." @@ -240,6 +250,22 @@ if(HTTPLIB_COMPILE) message(FATAL_ERROR "Failed when trying to split cpp-httplib with the Python script.\n${_httplib_split_error}") endif() + # If building modules, also generate the module file + if(HTTPLIB_BUILD_MODULES) + # Put the generate_module script into the build dir + configure_file(generate_module.py "${CMAKE_CURRENT_BINARY_DIR}/generate_module.py" + COPYONLY + ) + # Generate the module file + execute_process(COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/generate_module.py" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ERROR_VARIABLE _httplib_module_error + ) + if(_httplib_module_error) + message(FATAL_ERROR "Failed when trying to generate cpp-httplib module with the Python script.\n${_httplib_module_error}") + endif() + endif() + # split.py puts output in "out" set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out") add_library(${PROJECT_NAME} ${HTTPLIB_LIB_TYPE} "${_httplib_build_includedir}/httplib.cc") @@ -248,6 +274,13 @@ if(HTTPLIB_COMPILE) $ $ ) + + # Add C++20 module support if requested + # Include from separate file to prevent parse errors on older CMake versions + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28") + include(cmake/modules.cmake) + endif() + set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${${PROJECT_NAME}_VERSION} @@ -264,8 +297,12 @@ endif() # Only useful if building in-tree, versus using it from an installation. add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) -# Require C++11 -target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11) +# Require C++11, or C++20 if modules are enabled +if(HTTPLIB_BUILD_MODULES) + target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_20) +else() + target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11) +endif() target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC} $ @@ -337,7 +374,11 @@ if(HTTPLIB_INSTALL) # Creates the export httplibTargets.cmake # This is strictly what holds compilation requirements # and linkage information (doesn't find deps though). - install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets) + if(HTTPLIB_BUILD_MODULES) + install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/httplib/modules CXX_MODULES_BMI DESTINATION ${CMAKE_INSTALL_LIBDIR}/httplib/modules) + else() + install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets) + endif() install(FILES "${_httplib_build_includedir}/httplib.h" TYPE INCLUDE) @@ -366,6 +407,10 @@ if(HTTPLIB_INSTALL) include(CPack) endif() +if(HTTPLIB_BUILD_MODULES AND NOT HTTPLIB_COMPILE) + message(FATAL_ERROR "HTTPLIB_BUILD_MODULES requires HTTPLIB_COMPILE to be ON.") +endif() + if(HTTPLIB_TEST) include(CTest) add_subdirectory(test) diff --git a/cmake/modules.cmake b/cmake/modules.cmake new file mode 100644 index 0000000..bea4452 --- /dev/null +++ b/cmake/modules.cmake @@ -0,0 +1,16 @@ +# This file contains C++20 module support requiring CMake 3.28+ +# Included conditionally to prevent parse errors on older CMake versions + +if(HTTPLIB_BUILD_MODULES) + if(POLICY CMP0155) + cmake_policy(SET CMP0155 NEW) + endif() + + set(CMAKE_CXX_SCAN_FOR_MODULES ON) + + target_sources(${PROJECT_NAME} + PUBLIC + FILE_SET CXX_MODULES FILES + "${_httplib_build_includedir}/httplib.cppm" + ) +endif() diff --git a/generate_module.py b/generate_module.py new file mode 100644 index 0000000..c6e71d9 --- /dev/null +++ b/generate_module.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +"""This script generates httplib.cppm module file from httplib.h.""" + +import os +import sys +from argparse import ArgumentParser, Namespace +from typing import List + +def main() -> None: + """Main entry point for the script.""" + + args_parser: ArgumentParser = ArgumentParser(description=__doc__) + args_parser.add_argument( + "-o", "--out", help="where to write the files (default: out)", default="out" + ) + args: Namespace = args_parser.parse_args() + + cur_dir: str = os.path.dirname(sys.argv[0]) + if not cur_dir: + cur_dir = '.' + lib_name: str = "httplib" + header_name: str = f"/{lib_name}.h" + # get the input file + in_file: str = f"{cur_dir}{header_name}" + # get the output file + cppm_out: str = f"{args.out}/{lib_name}.cppm" + + # if the modification time of the out file is after the in file, + # don't generate (as it is already finished) + do_generate: bool = True + + if os.path.exists(cppm_out): + in_time: float = os.path.getmtime(in_file) + out_time: float = os.path.getmtime(cppm_out) + do_generate: bool = in_time > out_time + + if do_generate: + with open(in_file) as f: + lines: List[str] = f.readlines() + + os.makedirs(args.out, exist_ok=True) + + # Find the Headers and Declaration comment markers + headers_start: int = -1 + declaration_start: int = -1 + for i, line in enumerate(lines): + if ' * Headers' in line: + headers_start = i - 1 # Include the /* line + elif ' * Declaration' in line: + declaration_start = i - 1 # Stop before the /* line + break + + with open(cppm_out, 'w') as fm: + # Write module file + fm.write("module;\n\n") + + # Write global module fragment (from Headers to Declaration comment) + # Filter out 'using' declarations to avoid conflicts + if headers_start >= 0 and declaration_start >= 0: + for i in range(headers_start, declaration_start): + line: str = lines[i] + if 'using' not in line: + fm.write(line) + + fm.write("\nexport module httplib;\n\n") + fm.write("export extern \"C++\" {\n") + fm.write(f"{' ' * 4}#include \"httplib.h\"\n") + fm.write("}\n") + + print(f"Wrote {cppm_out}") + else: + print(f"{cppm_out} is up to date") + + +if __name__ == "__main__": + main() diff --git a/split.py b/split.py index d3682ef..660b45a 100755 --- a/split.py +++ b/split.py @@ -7,15 +7,14 @@ import sys from argparse import ArgumentParser, Namespace from typing import List +BORDER: str = '// ----------------------------------------------------------------------------' def main() -> None: """Main entry point for the script.""" - BORDER: str = '// ----------------------------------------------------------------------------' args_parser: ArgumentParser = ArgumentParser(description=__doc__) args_parser.add_argument( - "-e", "--extension", help="extension of the implementation file (default: cc)", - default="cc" + "-e", "--extension", help="extension of the implementation file (default: cc)", default="cc" ) args_parser.add_argument( "-o", "--out", help="where to write the files (default: out)", default="out" @@ -25,14 +24,14 @@ def main() -> None: cur_dir: str = os.path.dirname(sys.argv[0]) if not cur_dir: cur_dir = '.' - lib_name: str = 'httplib' + lib_name: str = "httplib" header_name: str = f"/{lib_name}.h" source_name: str = f"/{lib_name}.{args.extension}" # get the input file - in_file: str = cur_dir + header_name + in_file: str = f"{cur_dir}{header_name}" # get the output file - h_out: str = args.out + header_name - cc_out: str = args.out + source_name + h_out: str = f"{args.out}{header_name}" + cc_out: str = f"{args.out}{source_name}" # if the modification time of the out file is after the in file, # don't split (as it is already finished) @@ -51,18 +50,23 @@ def main() -> None: in_implementation: bool = False cc_out: str = args.out + source_name + with open(h_out, 'w') as fh, open(cc_out, 'w') as fc: - fc.write('#include "httplib.h"\n') - fc.write('namespace httplib {\n') + # Write source file + fc.write("#include \"httplib.h\"\n") + fc.write("namespace httplib {\n") + + # Process lines for header and source split for line in lines: is_border_line: bool = BORDER in line if is_border_line: in_implementation: bool = not in_implementation elif in_implementation: - fc.write(line.replace('inline ', '')) + fc.write(line.replace("inline ", "")) else: fh.write(line) - fc.write('} // namespace httplib\n') + + fc.write("} // namespace httplib\n") print(f"Wrote {h_out} and {cc_out}") else: