Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
81 changes: 81 additions & 0 deletions .github/workflows/update-module-exports.yml
Copy link
Owner

@yhirose yhirose Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this GitHub Action. But after further consideration, I now think this process should be done in CMakeLists.txt. You could generate modules/httplib.cppm under this line

if(HTTPLIB_COMPILE)

We actually do the similar to generate httplib.h and httplib.cc with split.py and put them in a distribution package. By doing this, we no longer need to keep modules/httplib.cppm in this repository. It will be created on the fly only when necessary.

@sum01 @jimmy-park Is my comment above correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be difficult to parse an especially large file without syntax analysis or parsing library. I don't have a lot of experience with this sort of thing, and because the header contains things like method definitions outside of the class it seems to be a very complex task. Then including a C++ parser would require a dependency even for a source generation script which would be additional bloat.

I guess this could be done by attaching to the header file that split.py generates, but I haven't actually seen what that looks like.

As for dynamically generating the module, this was raised before on the Dear ImGui library which currently does something like this. I think this is a bad choice for module API (having a static file is obviously best for consumers to be able to just read it from a distance without additional interaction, i.e. a "what-you-see-is-what-you-get" sort of API), but ultimately as you are in charge I'll look into this if you prefer it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking more into this and I think if we want to allow CMake to compile the module if it's generated into an "out" directory, we have to force the out directory to be named "out" so that it can be written into the CMakeLists.txt script

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway @yhirose do you have any opinion on the output directory having a hard-coded name?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sum01 @jimmy-park @Tachi107 could you please answer this question?

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Update Module Exports

on:
pull_request:
paths:
- 'httplib.h'
push:
branches:
- master
- main
paths:
- 'httplib.h'

jobs:
update-exports:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper diff

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Check for changes in httplib.h
id: check_changes
run: |
if git diff --name-only HEAD~1 HEAD | grep -q "httplib.h"; then
echo "changes=true" >> $GITHUB_OUTPUT
else
echo "changes=false" >> $GITHUB_OUTPUT
fi

- name: Update module exports
if: steps.check_changes.outputs.changes == 'true'
run: |
python3 update_modules.py

- name: Check if module file was modified
if: steps.check_changes.outputs.changes == 'true'
id: check_module_changes
run: |
if git diff --quiet modules/httplib.cppm; then
echo "modified=false" >> $GITHUB_OUTPUT
else
echo "modified=true" >> $GITHUB_OUTPUT
fi

- name: Commit changes
if: steps.check_module_changes.outputs.modified == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add modules/httplib.cppm
git commit -m "chore: update module exports for httplib.h changes"

- name: Push changes (for push events)
if: steps.check_module_changes.outputs.modified == 'true' && github.event_name == 'push'
run: |
git push

- name: Push changes (for pull requests)
if: steps.check_module_changes.outputs.modified == 'true' && github.event_name == 'pull_request'
run: |
git push origin HEAD:${{ github.head_ref }}

- name: Add comment to PR
if: steps.check_module_changes.outputs.modified == 'true' && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '✅ Module exports have been automatically updated based on changes to `httplib.h`.'
})
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ 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)
option(HTTPLIB_BUILD_MODULES "Build httplib modules (requires HTTPLIB_COMPILE to be ON)." OFF)
# 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."
Expand Down Expand Up @@ -365,3 +366,10 @@ if(HTTPLIB_TEST)
include(CTest)
add_subdirectory(test)
endif()

if(HTTPLIB_BUILD_MODULES)
if(NOT HTTPLIB_COMPILE)
message(FATAL_ERROR "HTTPLIB_BUILD_MODULES requires HTTPLIB_COMPILE to be ON.")
endif()
add_subdirectory(modules)
endif()
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,43 @@ $ ./split.py
Wrote out/httplib.h and out/httplib.cc
```

Build C++ Modules
-----------------

If using CMake, it is possible to build this as a C++20 module using the `HTTPLIB_BUILD_MODULES` option (which requires `HTTPLIB_COMPILE` to be enabled).

#### Server (Multi-threaded)
```cpp
import httplib;

using httplib::Request;
using httplib::Response;
using httplib::SSLServer;

SSLServer svr;

svr.Get("/hi", []([[maybe_unused]] const Request& req, Response& res) -> void {
res.set_content("Hello World!", "text/plain");
});

svr.listen("0.0.0.0", 8080);
```

#### Client
```cpp
import httplib;

using httplib::Client;
using httplib::Result;

Client cli("https://yhirose.github.io");

if (Result res = cli.Get("/hi")) {
res->status;
res->body;
}
```

Dockerfile for Static HTTP Server
---------------------------------

Expand Down
25 changes: 25 additions & 0 deletions modules/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
add_library(httplib_module)

target_sources(httplib_module
PUBLIC
FILE_SET CXX_MODULES FILES
httplib.cppm
)

target_compile_features(httplib_module PUBLIC cxx_std_20)

target_include_directories(httplib_module PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)

add_library(httplib::module ALIAS httplib_module)

# Installation
install(TARGETS httplib_module
EXPORT ${PROJECT_NAME}Targets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/httplib/modules
)
93 changes: 93 additions & 0 deletions modules/httplib.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// httplib.cppm
//
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
// MIT License
//

module;

#include "../httplib.h"

export module httplib;

export namespace httplib {
using httplib::SSLVerifierResponse;
using httplib::StatusCode;
using httplib::Headers;
using httplib::Params;
using httplib::Match;
using httplib::DownloadProgress;
using httplib::UploadProgress;
using httplib::Response;
using httplib::ResponseHandler;
using httplib::FormData;
using httplib::FormField;
using httplib::FormFields;
using httplib::FormFiles;
using httplib::MultipartFormData;
using httplib::UploadFormData;
using httplib::UploadFormDataItems;
using httplib::DataSink;
using httplib::ContentProvider;
using httplib::ContentProviderWithoutLength;
using httplib::ContentProviderResourceReleaser;
using httplib::FormDataProvider;
using httplib::FormDataProviderItems;
using httplib::ContentReceiverWithProgress;
using httplib::ContentReceiver;
using httplib::FormDataHeader;
using httplib::ContentReader;
using httplib::Range;
using httplib::Ranges;
using httplib::Request;
using httplib::Response;
using httplib::Error;
using httplib::to_string;
using httplib::operator<<;
using httplib::Stream;
using httplib::TaskQueue;
using httplib::ThreadPool;
using httplib::Logger;
using httplib::ErrorLogger;
using httplib::SocketOptions;
using httplib::default_socket_options;
using httplib::status_message;
using httplib::get_bearer_token_auth;
using httplib::Server;
using httplib::Result;
using httplib::ClientConnection;
using httplib::ClientImpl;
using httplib::Client;

#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
using httplib::SSLServer;
using httplib::SSLClient;
#endif

using httplib::hosted_at;
using httplib::encode_uri_component;
using httplib::encode_uri;
using httplib::decode_uri_component;
using httplib::decode_uri;
using httplib::encode_path_component;
using httplib::decode_path_component;
using httplib::encode_query_component;
using httplib::decode_query_component;
using httplib::append_query_params;
using httplib::make_range_header;
using httplib::make_basic_authentication_header;

using httplib::get_client_ip;

namespace stream {
using httplib::stream::Result;
using httplib::stream::Get;
using httplib::stream::Post;
using httplib::stream::Put;
using httplib::stream::Patch;
using httplib::stream::Delete;
using httplib::stream::Head;
using httplib::stream::Options;
}
}
110 changes: 59 additions & 51 deletions split.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,74 @@

"""This script splits httplib.h into .h and .cc parts."""

import argparse
import os
import sys
from argparse import ArgumentParser, Namespace
from typing import List

border = '// ----------------------------------------------------------------------------'

args_parser = argparse.ArgumentParser(description=__doc__)
args_parser.add_argument(
"-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"
)
args = args_parser.parse_args()
def main() -> None:
"""Main entry point for the script."""
BORDER: str = '// ----------------------------------------------------------------------------'

cur_dir = os.path.dirname(sys.argv[0])
lib_name = 'httplib'
header_name = '/' + lib_name + '.h'
source_name = '/' + lib_name + '.' + args.extension
# get the input file
in_file = cur_dir + header_name
# get the output file
h_out = args.out + header_name
cc_out = args.out + source_name
args_parser: ArgumentParser = ArgumentParser(description=__doc__)
args_parser.add_argument(
"-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"
)
args: Namespace = args_parser.parse_args()

# if the modification time of the out file is after the in file,
# don't split (as it is already finished)
do_split = True
cur_dir: str = os.path.dirname(sys.argv[0])
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
# get the output file
h_out: str = args.out + header_name
cc_out: str = args.out + source_name

if os.path.exists(h_out):
in_time = os.path.getmtime(in_file)
out_time = os.path.getmtime(h_out)
do_split = in_time > out_time
# if the modification time of the out file is after the in file,
# don't split (as it is already finished)
do_split: bool = True

if do_split:
with open(in_file) as f:
lines = f.readlines()
if os.path.exists(h_out):
in_time: float = os.path.getmtime(in_file)
out_time: float = os.path.getmtime(h_out)
do_split: bool = in_time > out_time

python_version = sys.version_info[0]
if python_version < 3:
os.makedirs(args.out)
if do_split:
with open(in_file) as f:
lines: List[str] = f.readlines()

python_version: int = sys.version_info[0]
if python_version < 3:
os.makedirs(args.out)
else:
os.makedirs(args.out, exist_ok=True)

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')
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 ', ''))
else:
fh.write(line)
fc.write('} // namespace httplib\n')

print(f"Wrote {h_out} and {cc_out}")
else:
os.makedirs(args.out, exist_ok=True)
print(f"{h_out} and {cc_out} are up to date")

in_implementation = False
cc_out = 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')
for line in lines:
is_border_line = border in line
if is_border_line:
in_implementation = not in_implementation
elif in_implementation:
fc.write(line.replace('inline ', ''))
else:
fh.write(line)
fc.write('} // namespace httplib\n')

print("Wrote {} and {}".format(h_out, cc_out))
else:
print("{} and {} are up to date".format(h_out, cc_out))
if __name__ == "__main__":
main()
Loading