This file contains the coding conventions, build instructions, and common pitfalls that apply across the repository. Detailed architecture and per-module design documentation lives in doc/docs/ — see the index at the bottom.
- .h files should include only things that are supposed to be shared with outside. Try best to minimize other things in .h files.
- For classes defined entirely within a .cpp file (not exposed via header), write all method bodies inline inside the class body. Do not separate declarations and out-of-class definitions. Let all members be public.
- .cc extension is used for assistant programs that are not included in normal compilation.
- Log lines can be written up to 2x the normal code line width, reducing unnecessary line breaks.
- Use backtick (
`) as a variable placeholder inside format strings to separate text from variables. Example:LOG_WARN("count ` total ", VALUE(count), VALUE(total). - Trailing backticks at the end of a format string are unnecessary — extra arguments beyond the number of backticks are output automatically. Example:
"failed, ", VALUE(st)not"failed, `", VALUE(st). - Formats are not actually defined in format-strings, instead they are defined by wrappers of the variables. Example:
LOG_WARN("count ` total ", DEC(count).comma(true), DEC(total).comma(true)will output the integers with commas. - Users can define custom output operators for their variables / objects.
VALUE(x)outputs both the variable name and its value (e.g.VALUE(st)→st=3), which aids debugging.- Never add redundant name prefixes before
VALUE()since it already includes the variable name. Example: use"failed, ", VALUE(st)not"failed, st=", VALUE(st). - Use judgment on whether
VALUE()is needed: opaque values like status codes benefit fromVALUE()(e.g.VALUE(st)), but context-clear values like loop indices or counts can omit it.
- When
LOG_ERRORandreturnare used together (possibly witherrnoset in between), preferLOG_ERROR_RETURN(errno_val, retval, ...)instead. - If the log message should also print the current errno, use
LOG_ERRNO_RETURN(errno_val, retval, ...). - Log at every error detection point: every place that detects an error (sets errno and returns an error code) must use
LOG_ERROR_RETURNto output a log and set errno — no silent error returns. - Exception — simple wrappers don't repeat: if a function is a thin wrapper that just propagates the return value of a callee which already logged the error, do not log again or re-set errno. Example:
if (sub_func() < 0) return -1;wheresub_funcalready usedLOG_ERROR_RETURN. - Abstraction boundary = new log: when the abstraction level changes (e.g. QAT hardware error → LZ4 compress failure), a new
LOG_ERROR_RETURNis appropriate to re-describe the error at the higher level, and errno may be re-set. If the lower layer already set a meaningful errno, useLOG_ERRNO_RETURNto output it. - if + LOG_ERROR_RETURN on separate lines: when an
ifbody consists solely ofLOG_ERROR_RETURNorLOG_ERRNO_RETURN, omit braces and put the macro on its own line. Example:Not:if (!ptr) LOG_ERROR_RETURN(ENOMEM, -1, "allocation failed");
if (!ptr) { LOG_ERROR_RETURN(ENOMEM, -1, "allocation failed"); }
Use RAII as long as it is not troublesome.
- Use
DEFERmacro for resource cleanup in initialization functions (e.g.create()patterns) to avoid repetitive manual cleanup on each error path. - Use the
bool ok = falsepattern: each DEFER checks!okbefore cleaning up; on success setok = trueto cancel all deferred cleanups. - When multiple consecutive
DEFERstatements serve the same logical purpose, combine them into a singleDEFER({ ... })block.
- Use
SCOPED_LOCK(lock)instead of manuallock.lock()/lock.unlock()to ensure the lock is always released, even on early returns or exceptions. - Applies to any lock type that
photon::lockersupports (spinlock, mutex, etc.).
- Prefer fixed-width integer types (
uint8_t,uint16_t,uint32_t,uint64_t, and their signed counterparts) over verbose built-in names (unsigned char,unsigned short,unsigned int,unsigned long,unsigned long long). - The only common exceptions are
size_tfor byte counts / array indices (where the width tracks the address space) andintfor small loop counters or values where a specific width has no semantic meaning. - For reading/writing a small fixed number of bytes (1, 2, 4, or 8) at a pointer, prefer a C-style cast over
memcpy/memset/std::copy. Example:*(uint32_t*)dst = value;to write,uint32_t v = *(const uint32_t*)src;to read. - This is clearer and produces tighter code than the equivalent
memcpy(&v, src, 4), and avoids the verbosity of*reinterpret_cast<uint32_t*>(dst)for such a common idiom. memset/memcpyremain appropriate for variable-length data and for zeroing structures larger than 8 bytes.- Alignment and byte order are the caller's responsibility; on little-endian hosts (x86_64, arm64) this reads/writes little-endian values directly, which matches many network and on-disk wire formats.
- When concatenating multiple parts into a string, prefer
estring().appends(a, b, c)overstd::string(a) + b + c. estring::appendsavoids creating intermediatestd::stringtemporaries and accepts mixed types (string_view, integers, etc.) without manual conversion.- Example:
std::string key = estring().appends(host, ":", port);notstd::string key = std::string(host) + ":" + std::to_string(port); - For conditional parts, use
estring::make_conditional_cat_list(cond, parts...)which appends only whencondis true:estring().appends( path, estring::make_conditional_cat_list(has_query, "?", query));
read(buf, count)andwrite(buf, count)guarantee completion of the fullcountbytes unless an error or EOF occurs. They are atomic from the caller's perspective.recv(buf, count)andsend(buf, count)perform a single receive/send operation and may return fewer bytes than requested.- When an exact amount must be transferred, prefer
read/write. Userecv/sendonly when partial transfer is acceptable.
- Separate moves from changes: when moving code, only move. Do not rename, restructure, or optimize at the same time.
- Understand the original purpose before making changes. The existing layout is not accidental.
- When modifying or optimizing existing code, respect the original design decisions — data structures, interfaces, implementation patterns, etc. Only change what's necessary.
- Trim trailing white space(s) of every line.
- Trim excessive new lines at the end of every file.
- Debian/Ubuntu
sudo apt-get install git cmake libssl-dev libcurl4-openssl-dev libaio-dev zlib1g-dev libgtest-dev libgmock-dev libgflags-dev libfuse-dev libgsasl7-dev nasm- RHEL/CentOS/Fedora
sudo dnf install git cmake openssl-devel libcurl-devel libaio-devel zlib-devel gtest-devel gmock-devel gflags-devel fuse-devel gsasl-devel nasm- macOS
brew install cmake openssl@1.1 pkg-config gflags googletest gsasl nasm# Linux
cmake -B build -D PHOTON_BUILD_TESTING=ON -D CMAKE_BUILD_TYPE=MinSizeRel
# macOS
cmake -B build -D PHOTON_BUILD_TESTING=ON -D CMAKE_BUILD_TYPE=MinSizeRel -D PHOTON_CXX_STANDARD=17 -D OPENSSL_ROOT_DIR=/usr/local/opt/openssl@1.1cmake --build build -j 8
# Output: build/output/cd build && ctestThe include/photon/ directory contains symlinks to source headers, mirroring the source tree structure. All headers are included as:
#include <photon/photon.h>
#include <photon/common/alog.h>
#include <photon/thread/thread.h>
#include <photon/net/socket.h>Internal includes (within the same module) may use relative paths: #include "../alog.h". Cross-module includes always use <photon/module/header.h>.
The build system sets include/ as a public include directory for photon_shared and photon_static targets. Test and example targets link against photon_shared to inherit the include path.
Framework: Google Test (gtest). Tests include ../../test/gtest.h which wraps <gtest/gtest.h> and suppresses sign-compare warnings. Some tests also include ../../test/ci-tools.h for CI helpers.
Location: Each module has a test/ subdirectory (e.g., common/test/, thread/test/, net/http/test/). Test files are named test_<feature>.cpp or test-<feature>.cpp.
CMake pattern: Each test/ directory has its own CMakeLists.txt following this template:
add_executable(test-<name> <name>.cpp)
target_link_libraries(test-<name> PRIVATE photon_shared)
add_test(NAME test-<name> COMMAND $<TARGET_FILE:test-<name>>)To add a new test:
- Create
test_foo.cppin the appropriate<module>/test/directory - Include module headers via relative paths (
"../foo.h") and cross-module headers via<photon/module/header.h> - Add the three-line CMake block above to that directory's
CMakeLists.txt - Build and run:
cmake --build build -j 8 && cd build && ctest -R test-foo
Test initialization: Tests that exercise coroutine or I/O features must call photon::init() / photon::fini() in their main() or in a gtest fixture's SetUp/TearDown.
Using std::mutex instead of photon::mutex in coroutines: std::mutex blocks the OS thread (vCPU), stalling all coroutines on that vCPU. Always use photon::mutex (or photon_std::mutex) inside photon threads.
Calling photon APIs before photon::init(): The current OS thread must be initialized as a vCPU before any photon API (thread creation, sync primitives, I/O) can be used. Call photon::init() first. If using WorkPool, each worker OS thread is initialized automatically.
Forgetting photon::fini() on exit: Skips cleanup of ancillary threads (timestamp updater, etc.) and registered fini_hook callbacks.
Blocking syscalls in coroutines: Direct sleep(), poll(), select(), or blocking read()/write() on non-photon file descriptors will block the vCPU. Use photon's coroutine-aware I/O or thread_usleep() instead.
Stack overflow: Default coroutine stack is 8MB. Deep recursion or large stack-allocated buffers may exceed it. Use thread_create11() with an explicit stack_size parameter for coroutines that need more.
- Ownership:
ownershipparameter (typicallybool) controls whether wrapper objects delete the wrapped object on destruction - Timeout: All I/O operations accept
Timeoutobjects integrating withphoton::now(microsecond-precision global timestamp) - Error handling: Functions return -1 or nullptr on error, set
errno, and log viaLOG_ERROR_RETURN - Thread safety: Photon sync primitives (mutex, semaphore, condition_variable) are coroutine-aware; std:: equivalents block the OS thread
Descriptive architecture and per-module design documentation lives under doc/docs/ (source for the Docusaurus site). Read these on demand when working on the corresponding area:
| When working on... | Read |
|---|---|
| Overall architecture, module relationships | doc/docs/introduction/photon-architecture.md |
| Coroutine runtime, scheduler, sync primitives | doc/docs/api/thread.md, doc/docs/api/lock-and-synchronization.md, doc/docs/api/vcpu-and-multicore.md |
| Event engines (epoll / io_uring / kqueue), signal handling | doc/docs/api/vcpu-and-multicore.md (Event Engines section) |
| Socket, HTTP client/server, TLS, connection pool | doc/docs/api/network.md |
| Filesystem VFS, local fs, HTTP fs, cache layer | doc/docs/api/filesystem-and-io.md |
| RPC framework, zero-copy serialization | doc/docs/api/rpc.md |
| Logging, string utilities, containers, delegates | doc/docs/api/common.md |
| Redis / OSS / simple_dom (JSON/XML/YAML) | doc/docs/ecosystem/overview.md |
| Delegate / Callback internals | doc/docs/api/delegate_callback.md |
| std-compatible API | doc/docs/api/std-compatible-api.md |
| Building and integrating | doc/docs/introduction/how-to-build.md, doc/docs/introduction/how-to-integrate.md |
| Debugging | doc/docs/miscellaneous/debugging.md |