Skip to content

Commit bb56ded

Browse files
zmerlynnclaude
andcommitted
Add MANIFOLD_NO_IOSTREAM build-time option
Wraps iostream-using overloads in the public headers (manifold.h, manifoldc.h) and their impls (src/impl.cpp, bindings/c/manifoldc.cpp) with `#ifndef MANIFOLD_NO_IOSTREAM`. Adds an `option(MANIFOLD_NO_IOSTREAM)` (default OFF) that propagates the macro as a PUBLIC compile definition on the manifold target so consumers' own includes of manifold's headers also strip the iostream-using overloads. Default behavior unchanged when off. The bundled Clipper2 (when MANIFOLD_USE_BUILTIN_CLIPPER2=ON) carries a corresponding patch that adds `CLIPPER2_NO_IOSTREAM` macro guards to its public headers — tracking AngusJohnson/Clipper2#1094, drops once that PR lands and the SHA pin moves past it. When MANIFOLD_NO_IOSTREAM=ON, manifold sets CLIPPER2_NO_IOSTREAM=ON for the bundled Clipper2 build. CMake-level conflict guards: MANIFOLD_NO_IOSTREAM=ON is incompatible with MANIFOLD_DEBUG / MANIFOLD_TIMING (those use std::cout for diagnostic output), and currently requires MANIFOLD_TEST=OFF (test fixtures use <filesystem>/<fstream>; restriction will be lifted by a follow-up MANIFOLD_NO_FILESYSTEM option). Build-time check: src/no_iostream_check.cpp + a manifold_no_iostream_check OBJECT target compile manifold/manifold.h with MANIFOLD_NO_IOSTREAM defined. Always-on; fails the build if iostream usage creeps into the public header outside the macro guards. CI: new build_no_iostream cell on Ubuntu/clang exercising MANIFOLD_NO_IOSTREAM=ON + MANIFOLD_USE_BUILTIN_CLIPPER2=ON (the patch-application path). Motivation + the broader consumer cocktail this enables: elalish#1046 (comment) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5f95a3a commit bb56ded

13 files changed

Lines changed: 322 additions & 0 deletions

File tree

.github/workflows/manifold.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,46 @@ jobs:
175175
sudo cmake --install build
176176
./scripts/test-cmake.sh
177177
178+
build_no_iostream:
179+
name: NO_IOSTREAM (clang)
180+
timeout-minutes: 20
181+
runs-on: ubuntu-latest
182+
steps:
183+
- uses: actions/checkout@v6
184+
- name: Install apt packages
185+
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
186+
with:
187+
packages: cmake clang
188+
- name: Configure + build with MANIFOLD_NO_IOSTREAM=ON
189+
run: |
190+
cmake \
191+
-DCMAKE_BUILD_TYPE=Release \
192+
-DCMAKE_C_COMPILER=clang \
193+
-DCMAKE_CXX_COMPILER=clang++ \
194+
-DMANIFOLD_NO_IOSTREAM=ON \
195+
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
196+
-DMANIFOLD_STRICT=ON \
197+
-DMANIFOLD_TEST=OFF \
198+
-DMANIFOLD_PYBIND=OFF \
199+
. -B build
200+
cmake --build build
201+
- name: Verify conflict guards fire (NO_IOSTREAM + DEBUG)
202+
run: |
203+
if cmake -DMANIFOLD_NO_IOSTREAM=ON -DMANIFOLD_DEBUG=ON \
204+
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
205+
. -B /tmp/conflict-debug 2>&1; then
206+
echo "FAIL: configure should have failed for NO_IOSTREAM + DEBUG"
207+
exit 1
208+
fi
209+
- name: Verify conflict guards fire (NO_IOSTREAM + TEST)
210+
run: |
211+
if cmake -DMANIFOLD_NO_IOSTREAM=ON -DMANIFOLD_TEST=ON \
212+
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
213+
. -B /tmp/conflict-test 2>&1; then
214+
echo "FAIL: configure should have failed for NO_IOSTREAM + TEST"
215+
exit 1
216+
fi
217+
178218
build_gcc_codecov:
179219
name: code coverage
180220
timeout-minutes: 45

CMakeLists.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF)
4242
option(MANIFOLD_ASSERT "Enable assertions - requires MANIFOLD_DEBUG" OFF)
4343
option(MANIFOLD_TIMING "Enable Boolean3 phase timing without debug overhead" OFF)
4444
option(MANIFOLD_STRICT "Treat compile warnings as fatal build errors" OFF)
45+
option(
46+
MANIFOLD_NO_IOSTREAM
47+
"Strip iostream- and filesystem-using bits from the public API and tests; useful for freestanding/embedded builds. When ON, defines both MANIFOLD_NO_IOSTREAM and MANIFOLD_NO_FILESYSTEM."
48+
OFF
49+
)
4550
option(
4651
MANIFOLD_DOWNLOADS
4752
"Allow Manifold build to download missing dependencies"
@@ -100,6 +105,28 @@ mark_as_advanced(TRACY_MEMORY_USAGE)
100105
mark_as_advanced(MANIFOLD_FUZZ)
101106
mark_as_advanced(ASSIMP_ENABLE)
102107

108+
# MANIFOLD_NO_IOSTREAM strips iostream- and filesystem-using bits
109+
# from the public API. It's incompatible with options that *use*
110+
# iostream/filesystem for their diagnostic or test output:
111+
# * MANIFOLD_DEBUG / MANIFOLD_TIMING: debug/timing output via
112+
# std::cout.
113+
# * MANIFOLD_TEST: most test files (polygon_test, manifold_test,
114+
# polygon_fuzz, manifoldc_test) call iostream-using API or open
115+
# fstreams directly outside the wrapped test_main.cpp fixtures.
116+
if(MANIFOLD_NO_IOSTREAM)
117+
if(MANIFOLD_DEBUG OR MANIFOLD_TIMING)
118+
message(FATAL_ERROR
119+
"MANIFOLD_NO_IOSTREAM is incompatible with MANIFOLD_DEBUG / MANIFOLD_TIMING "
120+
"(those options use std::cout for diagnostic output).")
121+
endif()
122+
if(MANIFOLD_TEST)
123+
message(FATAL_ERROR
124+
"MANIFOLD_NO_IOSTREAM=ON requires MANIFOLD_TEST=OFF "
125+
"(several test files call iostream-using API or open fstreams "
126+
"directly; only test_main.cpp's fixtures are wrapped).")
127+
endif()
128+
endif()
129+
103130
# Always build position independent code for relocatability
104131
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
105132

bindings/c/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,14 @@ install(
4343
FILES include/manifold/manifoldc.h include/manifold/types.h
4444
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/manifold
4545
)
46+
47+
# Always-on build-time check: compiles manifold/manifoldc.h with
48+
# MANIFOLD_NO_IOSTREAM defined. Mirrors src/no_iostream_check.cpp's
49+
# role for the C++ public header.
50+
add_library(manifoldc_no_iostream_check OBJECT no_iostream_check.cpp)
51+
target_link_libraries(manifoldc_no_iostream_check PRIVATE manifoldc)
52+
if(NOT MANIFOLD_NO_IOSTREAM)
53+
target_compile_definitions(
54+
manifoldc_no_iostream_check PRIVATE MANIFOLD_NO_IOSTREAM
55+
)
56+
endif()

bindings/c/include/manifold/manifoldc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ void manifold_delete_rect(ManifoldRect* b);
542542
void manifold_delete_triangulation(ManifoldTriangulation* m);
543543
void manifold_delete_execution_context(ManifoldExecutionContext* ctx);
544544

545+
#ifndef MANIFOLD_NO_IOSTREAM
545546
// MeshIO / Export
546547

547548
// Import a manifold from a Wavefront obj file.
@@ -570,6 +571,7 @@ void manifold_write_obj(ManifoldManifold* manifold,
570571
// passing additional data into the callback.
571572
void manifold_meshgl64_write_obj(ManifoldMeshGL64* mesh,
572573
void (*callback)(char*, void*), void* args);
574+
#endif // !MANIFOLD_NO_IOSTREAM
573575
#ifdef __cplusplus
574576
}
575577
#endif

bindings/c/manifoldc.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
#include "manifold/manifoldc.h"
1616

17+
#ifndef MANIFOLD_NO_IOSTREAM
1718
#include <sstream>
19+
#endif
1820
#include <vector>
1921

2022
#include "conv.h"
@@ -1048,6 +1050,7 @@ void manifold_destruct_execution_context(ManifoldExecutionContext* ctx) {
10481050

10491051
// IO
10501052

1053+
#ifndef MANIFOLD_NO_IOSTREAM
10511054
ManifoldManifold* manifold_read_obj(void* mem, char* obj_file) {
10521055
std::istringstream iss(obj_file);
10531056
Manifold m = Manifold::ReadOBJ(iss);
@@ -1075,6 +1078,7 @@ void manifold_meshgl64_write_obj(ManifoldMeshGL64* mesh,
10751078
WriteOBJ(ss, *m);
10761079
callback(ss.str().data(), args);
10771080
}
1081+
#endif // !MANIFOLD_NO_IOSTREAM
10781082

10791083
#ifdef __cplusplus
10801084
}

bindings/c/no_iostream_check.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2026 The Manifold Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// Build-time check: compiles manifold/manifoldc.h with MANIFOLD_NO_IOSTREAM
16+
// defined. Fails the build if iostream usage creeps into the C-API
17+
// header outside the macro guards.
18+
19+
#include "manifold/manifoldc.h"

cmake/manifoldDeps.cmake

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ if(MANIFOLD_CROSS_SECTION)
119119
CACHE BOOL
120120
"Preempt cache default of USINGZ (we only use 2d)"
121121
)
122+
# When manifold is built with MANIFOLD_NO_IOSTREAM, also strip
123+
# iostream from the bundled Clipper2 — manifold doesn't call any
124+
# of Clipper2's stream operators internally, so passing this
125+
# through is safe regardless. The CLIPPER2_NO_IOSTREAM macro is
126+
# added by the carry-patch below; once Clipper2#1094 lands and
127+
# the SHA pin moves past it, the patch drops and the option is
128+
# honored natively.
129+
if(MANIFOLD_NO_IOSTREAM)
130+
set(
131+
CLIPPER2_NO_IOSTREAM
132+
ON
133+
CACHE BOOL
134+
"Strip iostream-using overloads from Clipper2 (set by manifold when MANIFOLD_NO_IOSTREAM=ON)"
135+
FORCE
136+
)
137+
endif()
122138
FetchContent_Declare(
123139
Clipper2
124140
GIT_REPOSITORY https://github.com/AngusJohnson/Clipper2.git
@@ -127,6 +143,13 @@ if(MANIFOLD_CROSS_SECTION)
127143
GIT_PROGRESS TRUE
128144
SOURCE_SUBDIR
129145
CPP
146+
# Carry-patch: tracks AngusJohnson/Clipper2#1094 (CLIPPER2_NO_IOSTREAM
147+
# macro guards). Drops once that PR lands and the SHA pin moves past it.
148+
PATCH_COMMAND
149+
git
150+
apply
151+
--ignore-whitespace
152+
${CMAKE_SOURCE_DIR}/cmake/patches/0001-clipper2-no-iostream.patch
130153
)
131154
FetchContent_MakeAvailable(Clipper2)
132155
set_property(
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
Carry-patch: add CLIPPER2_NO_IOSTREAM macro guards to Clipper2's
2+
public headers so manifold can build Clipper2 without iostream when
3+
MANIFOLD_NO_IOSTREAM is set (e.g., for wasm32-unknown-unknown
4+
consumers via the wasm-cxx-shim integration).
5+
6+
Tracks AngusJohnson/Clipper2#1094 — once that lands and manifold's
7+
Clipper2 SHA pin moves past it, this patch drops.
8+
9+
Generated against Clipper2 SHA 46f639177fe418f9689e8ddb74f08a870c71f5b4
10+
(the SHA manifoldDeps.cmake currently pins). The headers at this SHA
11+
are byte-identical to upstream main, so the patch tracks #1094's diff
12+
exactly.
13+
14+
Apply with: git apply -p1 0001-clipper2-no-iostream.patch
15+
16+
diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h
17+
index 99a5205..d3af0a0 100644
18+
--- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h
19+
+++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h
20+
@@ -14,7 +14,9 @@
21+
#include <cstdint>
22+
#include <vector>
23+
#include <string>
24+
+#ifndef CLIPPER2_NO_IOSTREAM
25+
#include <iostream>
26+
+#endif
27+
#include <algorithm>
28+
#include <numeric>
29+
#include <cmath>
30+
@@ -166,11 +168,13 @@ namespace Clipper2Lib
31+
32+
void SetZ(const z_type z_value) { z = z_value; }
33+
34+
+#ifndef CLIPPER2_NO_IOSTREAM
35+
friend std::ostream& operator<<(std::ostream& os, const Point& point)
36+
{
37+
os << point.x << "," << point.y << "," << point.z;
38+
return os;
39+
}
40+
+#endif
41+
42+
#else
43+
44+
@@ -203,11 +207,13 @@ namespace Clipper2Lib
45+
return Point(x * scale, y * scale);
46+
}
47+
48+
+#ifndef CLIPPER2_NO_IOSTREAM
49+
friend std::ostream& operator<<(std::ostream& os, const Point& point)
50+
{
51+
os << point.x << "," << point.y;
52+
return os;
53+
}
54+
+#endif
55+
#endif
56+
57+
friend bool operator==(const Point& a, const Point& b)
58+
@@ -396,10 +402,12 @@ namespace Clipper2Lib
59+
return result;
60+
}
61+
62+
+#ifndef CLIPPER2_NO_IOSTREAM
63+
friend std::ostream& operator<<(std::ostream& os, const Rect<T>& rect) {
64+
os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") ";
65+
return os;
66+
}
67+
+#endif
68+
};
69+
70+
template <typename T1, typename T2>
71+
@@ -498,6 +506,7 @@ namespace Clipper2Lib
72+
return Rect<T>(xmin, ymin, xmax, ymax);
73+
}
74+
75+
+#ifndef CLIPPER2_NO_IOSTREAM
76+
template <typename T>
77+
std::ostream& operator << (std::ostream& outstream, const Path<T>& path)
78+
{
79+
@@ -518,6 +527,7 @@ namespace Clipper2Lib
80+
outstream << p;
81+
return outstream;
82+
}
83+
+#endif
84+
85+
86+
template <typename T1, typename T2>
87+
diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h
88+
index fe1e299..deb1f7b 100644
89+
--- a/CPP/Clipper2Lib/include/clipper2/clipper.h
90+
+++ b/CPP/Clipper2Lib/include/clipper2/clipper.h
91+
@@ -309,6 +309,7 @@ namespace Clipper2Lib {
92+
return true;
93+
}
94+
95+
+#ifndef CLIPPER2_NO_IOSTREAM
96+
static void OutlinePolyPath(std::ostream& os,
97+
size_t idx, bool isHole, size_t count, const std::string& preamble)
98+
{
99+
@@ -338,6 +339,7 @@ namespace Clipper2Lib {
100+
if (pp.Child(i)->Count())
101+
details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " ");
102+
}
103+
+#endif
104+
105+
template<typename T, typename U>
106+
inline constexpr void MakePathGeneric(const T an_array,
107+
@@ -377,6 +379,7 @@ namespace Clipper2Lib {
108+
109+
} // end details namespace
110+
111+
+#ifndef CLIPPER2_NO_IOSTREAM
112+
inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp)
113+
{
114+
std::string plural = (pp.Count() == 1) ? " polygon." : " polygons.";
115+
@@ -399,6 +402,7 @@ namespace Clipper2Lib {
116+
if (!pp.Level()) os << std::endl;
117+
return os;
118+
}
119+
+#endif
120+
121+
inline Paths64 PolyTreeToPaths64(const PolyTree64& polytree)
122+
{

include/manifold/manifold.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,10 @@ using MeshGL = MeshGLP<float>;
273273
*/
274274
using MeshGL64 = MeshGLP<double, uint64_t>;
275275

276+
#ifndef MANIFOLD_NO_IOSTREAM
276277
MeshGL64 ReadOBJ(std::istream& stream);
277278
bool WriteOBJ(std::ostream& stream, const MeshGL64& mesh);
279+
#endif
278280

279281
/**
280282
* @brief This library's internal representation of an oriented, 2-manifold,
@@ -517,8 +519,10 @@ class Manifold {
517519
* ofile.close();
518520
* @endcode
519521
*/
522+
#ifndef MANIFOLD_NO_IOSTREAM
520523
static Manifold ReadOBJ(std::istream& stream);
521524
bool WriteOBJ(std::ostream& stream) const;
525+
#endif
522526

523527
/** @name Testing Hooks
524528
* These are just for internal testing.

src/CMakeLists.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ set(
8080
MANIFOLD_ASSERT
8181
MANIFOLD_TIMING
8282
MANIFOLD_CROSS_SECTION
83+
MANIFOLD_NO_IOSTREAM
8384
TRACY_ENABLE
8485
TRACY_MEMORY_USAGE
8586
)
@@ -94,6 +95,30 @@ else()
9495
target_compile_definitions(manifold PUBLIC MANIFOLD_PAR=-1)
9596
endif()
9697

98+
# MANIFOLD_NO_IOSTREAM also defines MANIFOLD_NO_FILESYSTEM (the two
99+
# track together in practice — `<filesystem>` paths are only useful
100+
# if you can `<fstream>` them, which needs iostream). Source code
101+
# uses the macros separately for semantic clarity (NO_IOSTREAM gates
102+
# the iostream-using public API; NO_FILESYSTEM gates filesystem use
103+
# in test_main.cpp's fixture helpers + main()).
104+
if(MANIFOLD_NO_IOSTREAM)
105+
target_compile_definitions(manifold PUBLIC MANIFOLD_NO_FILESYSTEM)
106+
endif()
107+
108+
# Always-on build-time check: compiles manifold/manifold.h with
109+
# MANIFOLD_NO_IOSTREAM defined. Fails if iostream usage creeps into
110+
# the public header outside the macro guards. Independent of the
111+
# MANIFOLD_NO_IOSTREAM option's value (just inherits manifold's other
112+
# PUBLIC defines via target_link_libraries; sets the macro PRIVATE
113+
# only when the option isn't already setting it PUBLIC).
114+
add_library(manifold_no_iostream_check OBJECT no_iostream_check.cpp)
115+
target_link_libraries(manifold_no_iostream_check PRIVATE manifold)
116+
if(NOT MANIFOLD_NO_IOSTREAM)
117+
target_compile_definitions(
118+
manifold_no_iostream_check PRIVATE MANIFOLD_NO_IOSTREAM
119+
)
120+
endif()
121+
97122
target_include_directories(
98123
manifold
99124
PUBLIC

0 commit comments

Comments
 (0)