Skip to content

Commit a834bc0

Browse files
committed
[Python] Export only PyInit_* from macOS extension modules
Each extension statically links protobuf, abseil and cel-cpp. On macOS, pybind_extension links with only `-undefined dynamic_lookup`, so those copies are exported, including thousands of weak definitions. dyld coalesces exported weak definitions across every loaded image, so importing cel_expr_python alongside another extension that ships protobuf (e.g. ortools) binds one runtime's code to the other's parse tables and crashes in AddDescriptors at import time. Wrap pybind_extension to add `-Wl,-exported_symbol,_PyInit_<name>` on macOS, matching the isolation Linux already gets from `-Wl,-Bsymbolic`, and add a macOS test that each extension exports only its init function. Fixes #115
1 parent 45eefa7 commit a834bc0

5 files changed

Lines changed: 141 additions & 3 deletions

File tree

‎cel_expr_python/BUILD‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension", "pybind_library")
1+
load("@pybind11_bazel//:build_defs.bzl", "pybind_library")
22
load("@rules_cc//cc:cc_library.bzl", "cc_library")
33
load("@rules_python//python:py_test.bzl", "py_test")
4+
load(":pybind_extension.bzl", "pybind_extension")
45

56
package(default_visibility = ["//visibility:public"])
67

@@ -209,6 +210,37 @@ py_test(
209210
}),
210211
)
211212

213+
py_test(
214+
name = "exported_symbols_test",
215+
srcs = ["exported_symbols_test.py"],
216+
data = [
217+
":cel",
218+
"//cel_expr_python/ext:ext_bindings",
219+
"//cel_expr_python/ext:ext_encoders",
220+
"//cel_expr_python/ext:ext_lists",
221+
"//cel_expr_python/ext:ext_math",
222+
"//cel_expr_python/ext:ext_optional",
223+
"//cel_expr_python/ext:ext_proto",
224+
"//cel_expr_python/ext:ext_strings",
225+
],
226+
deps = [
227+
"@com_google_absl_py//absl/testing:absltest",
228+
"@com_google_absl_py//absl/testing:parameterized",
229+
] + select({
230+
"@platforms//os:windows": [],
231+
"//conditions:default": [
232+
":cel",
233+
"//cel_expr_python/ext:ext_bindings",
234+
"//cel_expr_python/ext:ext_encoders",
235+
"//cel_expr_python/ext:ext_lists",
236+
"//cel_expr_python/ext:ext_math",
237+
"//cel_expr_python/ext:ext_optional",
238+
"//cel_expr_python/ext:ext_proto",
239+
"//cel_expr_python/ext:ext_strings",
240+
],
241+
}),
242+
)
243+
212244
# Platform definition for macOS x86_64.
213245
# Used for cross-compiling from arm64 macOS hosts (Apple Silicon) to x86_64.
214246
# This is passed to Bazel via --platforms in setup.py when building x86_64 wheels.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2026 Google LLC
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+
"""Tests that macOS extension modules export only their init function.
16+
17+
Exported copies of the statically linked protobuf and abseil runtimes are
18+
coalesced by dyld with any other copy loaded in the process (for example
19+
ortools'), which crashes at import time.
20+
"""
21+
22+
import subprocess
23+
import sys
24+
import types
25+
26+
from absl.testing import absltest
27+
from absl.testing import parameterized
28+
from cel_expr_python import cel
29+
from cel_expr_python.ext import ext_bindings
30+
from cel_expr_python.ext import ext_encoders
31+
from cel_expr_python.ext import ext_lists
32+
from cel_expr_python.ext import ext_math
33+
from cel_expr_python.ext import ext_optional
34+
from cel_expr_python.ext import ext_proto
35+
from cel_expr_python.ext import ext_strings
36+
37+
_MODULES = (
38+
cel,
39+
ext_bindings,
40+
ext_encoders,
41+
ext_lists,
42+
ext_math,
43+
ext_optional,
44+
ext_proto,
45+
ext_strings,
46+
)
47+
48+
49+
@absltest.skipUnless(sys.platform == "darwin", "macOS-specific linkage")
50+
class ExportedSymbolsTest(parameterized.TestCase):
51+
52+
@parameterized.parameters(*_MODULES)
53+
def test_exports_only_init_function(self, module: types.ModuleType):
54+
output = subprocess.run(
55+
["nm", "-gU", module.__file__],
56+
check=True,
57+
capture_output=True,
58+
text=True,
59+
).stdout
60+
exported = {line.split()[-1] for line in output.splitlines() if line}
61+
init_symbol = "_PyInit_" + module.__name__.rpartition(".")[2]
62+
self.assertEqual(exported, {init_symbol})
63+
64+
65+
if __name__ == "__main__":
66+
absltest.main()

‎cel_expr_python/ext/BUILD‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension")
1+
load("//cel_expr_python:pybind_extension.bzl", "pybind_extension")
22

33
package(default_visibility = ["//visibility:public"])
44

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2026 Google LLC
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+
# http://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+
"""pybind_extension wrapper that keeps statically linked dependencies private."""
16+
17+
load("@pybind11_bazel//:build_defs.bzl", _pybind_extension = "pybind_extension")
18+
19+
def pybind_extension(name, linkopts = [], **kwargs):
20+
"""Builds a pybind11 extension module that exports only its init function.
21+
22+
Each extension statically links its own copy of protobuf, abseil and
23+
cel-cpp. On macOS, dyld coalesces exported weak definitions across every
24+
loaded image, so exporting those copies lets them bind to another
25+
extension's protobuf or abseil (e.g. ortools') and crash at import time.
26+
Linux is unaffected because pybind_extension links with -Bsymbolic there.
27+
28+
Args:
29+
name: The name of the extension module.
30+
linkopts: Additional linker options.
31+
**kwargs: Passed through to pybind11_bazel's pybind_extension.
32+
"""
33+
_pybind_extension(
34+
name = name,
35+
linkopts = linkopts + select({
36+
"@platforms//os:osx": ["-Wl,-exported_symbol,_PyInit_" + name],
37+
"//conditions:default": [],
38+
}),
39+
**kwargs
40+
)

‎custom_ext/BUILD‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension")
21
load("@rules_python//python:py_library.bzl", "py_library")
32
load("@rules_python//python:py_test.bzl", "py_test")
3+
load("//cel_expr_python:pybind_extension.bzl", "pybind_extension")
44

55
package(default_visibility = ["//visibility:private"])
66

0 commit comments

Comments
 (0)