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 <andrea@pappacoda.it>

* Update CMakeLists.txt

Co-authored-by: Andrea Pappacoda <andrea@pappacoda.it>

* Split scripts into split.py and generate_module.py

---------

Co-authored-by: Andrea Pappacoda <andrea@pappacoda.it>
This commit is contained in:
Miko
2026-02-02 11:27:09 -05:00
committed by GitHub
parent 6be32a540d
commit 1942e0ef01
4 changed files with 156 additions and 14 deletions

View File

@@ -5,6 +5,7 @@
* HTTPLIB_USE_ZLIB_IF_AVAILABLE (default on) * HTTPLIB_USE_ZLIB_IF_AVAILABLE (default on)
* HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on) * HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on)
* HTTPLIB_USE_ZSTD_IF_AVAILABLE (default on) * HTTPLIB_USE_ZSTD_IF_AVAILABLE (default on)
* HTTPLIB_BUILD_MODULES (default off)
* HTTPLIB_REQUIRE_OPENSSL (default off) * HTTPLIB_REQUIRE_OPENSSL (default off)
* HTTPLIB_REQUIRE_ZLIB (default off) * HTTPLIB_REQUIRE_ZLIB (default off)
* HTTPLIB_REQUIRE_BROTLI (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_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_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) 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 # Defaults to static library but respects standard BUILD_SHARED_LIBS if set
include(CMakeDependentOption) include(CMakeDependentOption)
cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only." 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}") message(FATAL_ERROR "Failed when trying to split cpp-httplib with the Python script.\n${_httplib_split_error}")
endif() 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" # split.py puts output in "out"
set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out") set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out")
add_library(${PROJECT_NAME} ${HTTPLIB_LIB_TYPE} "${_httplib_build_includedir}/httplib.cc") add_library(${PROJECT_NAME} ${HTTPLIB_LIB_TYPE} "${_httplib_build_includedir}/httplib.cc")
@@ -248,6 +274,13 @@ if(HTTPLIB_COMPILE)
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h> $<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h> $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h>
) )
# 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} set_target_properties(${PROJECT_NAME}
PROPERTIES PROPERTIES
VERSION ${${PROJECT_NAME}_VERSION} VERSION ${${PROJECT_NAME}_VERSION}
@@ -264,8 +297,12 @@ endif()
# Only useful if building in-tree, versus using it from an installation. # Only useful if building in-tree, versus using it from an installation.
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
# Require C++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) target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
endif()
target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC} target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC}
$<BUILD_INTERFACE:${_httplib_build_includedir}> $<BUILD_INTERFACE:${_httplib_build_includedir}>
@@ -337,7 +374,11 @@ if(HTTPLIB_INSTALL)
# Creates the export httplibTargets.cmake # Creates the export httplibTargets.cmake
# This is strictly what holds compilation requirements # This is strictly what holds compilation requirements
# and linkage information (doesn't find deps though). # and linkage information (doesn't find deps though).
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) install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets)
endif()
install(FILES "${_httplib_build_includedir}/httplib.h" TYPE INCLUDE) install(FILES "${_httplib_build_includedir}/httplib.h" TYPE INCLUDE)
@@ -366,6 +407,10 @@ if(HTTPLIB_INSTALL)
include(CPack) include(CPack)
endif() 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) if(HTTPLIB_TEST)
include(CTest) include(CTest)
add_subdirectory(test) add_subdirectory(test)

16
cmake/modules.cmake Normal file
View File

@@ -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()

77
generate_module.py Normal file
View File

@@ -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()

View File

@@ -7,15 +7,14 @@ import sys
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace
from typing import List from typing import List
BORDER: str = '// ----------------------------------------------------------------------------'
def main() -> None: def main() -> None:
"""Main entry point for the script.""" """Main entry point for the script."""
BORDER: str = '// ----------------------------------------------------------------------------'
args_parser: ArgumentParser = ArgumentParser(description=__doc__) args_parser: ArgumentParser = ArgumentParser(description=__doc__)
args_parser.add_argument( args_parser.add_argument(
"-e", "--extension", help="extension of the implementation file (default: cc)", "-e", "--extension", help="extension of the implementation file (default: cc)", default="cc"
default="cc"
) )
args_parser.add_argument( args_parser.add_argument(
"-o", "--out", help="where to write the files (default: out)", default="out" "-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]) cur_dir: str = os.path.dirname(sys.argv[0])
if not cur_dir: if not cur_dir:
cur_dir = '.' cur_dir = '.'
lib_name: str = 'httplib' lib_name: str = "httplib"
header_name: str = f"/{lib_name}.h" header_name: str = f"/{lib_name}.h"
source_name: str = f"/{lib_name}.{args.extension}" source_name: str = f"/{lib_name}.{args.extension}"
# get the input file # get the input file
in_file: str = cur_dir + header_name in_file: str = f"{cur_dir}{header_name}"
# get the output file # get the output file
h_out: str = args.out + header_name h_out: str = f"{args.out}{header_name}"
cc_out: str = args.out + source_name cc_out: str = f"{args.out}{source_name}"
# if the modification time of the out file is after the in file, # if the modification time of the out file is after the in file,
# don't split (as it is already finished) # don't split (as it is already finished)
@@ -51,18 +50,23 @@ def main() -> None:
in_implementation: bool = False in_implementation: bool = False
cc_out: str = args.out + source_name cc_out: str = args.out + source_name
with open(h_out, 'w') as fh, open(cc_out, 'w') as fc: with open(h_out, 'w') as fh, open(cc_out, 'w') as fc:
fc.write('#include "httplib.h"\n') # Write source file
fc.write('namespace httplib {\n') fc.write("#include \"httplib.h\"\n")
fc.write("namespace httplib {\n")
# Process lines for header and source split
for line in lines: for line in lines:
is_border_line: bool = BORDER in line is_border_line: bool = BORDER in line
if is_border_line: if is_border_line:
in_implementation: bool = not in_implementation in_implementation: bool = not in_implementation
elif in_implementation: elif in_implementation:
fc.write(line.replace('inline ', '')) fc.write(line.replace("inline ", ""))
else: else:
fh.write(line) fh.write(line)
fc.write('} // namespace httplib\n')
fc.write("} // namespace httplib\n")
print(f"Wrote {h_out} and {cc_out}") print(f"Wrote {h_out} and {cc_out}")
else: else: