Просмотр исходного кода

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>
Miko 2 дней назад
Родитель
Сommit
1942e0ef01
4 измененных файлов с 156 добавлено и 14 удалено
  1. 48 3
      CMakeLists.txt
  2. 16 0
      cmake/modules.cmake
  3. 77 0
      generate_module.py
  4. 15 11
      split.py

+ 48 - 3
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)
 			$<BUILD_INTERFACE:${_httplib_build_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}
 		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}
 	$<BUILD_INTERFACE:${_httplib_build_includedir}>
@@ -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)

+ 16 - 0
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()

+ 77 - 0
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()

+ 15 - 11
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: