gave up vibe coding. confidence isn't correctness.
...and sycophantic toasters are bad collaborators
diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl
index 3465ceb..8d76cdb 100644
--- a/python/private/cc/py_extension_rule.bzl
+++ b/python/private/cc/py_extension_rule.bzl
@@ -5,11 +5,13 @@
load("//python/private:attr_builders.bzl", "attrb")
load("//python/private:attributes.bzl", "COMMON_ATTRS")
load("//python/private:py_info.bzl", "PyInfo")
+load("//python/private:py_internal.bzl", "py_internal")
load("//python/private:reexports.bzl", "BuiltinPyInfo")
load("//python/private:rule_builders.bzl", "ruleb")
load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
def _py_extension_impl(ctx):
+ module_name = ctx.attr.module_name or ctx.label.name
cc_toolchain = ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc
feature_configuration = cc_common.configure_features(
ctx = ctx,
@@ -24,62 +26,75 @@
cc_infos = static_deps_infos + dynamic_deps_infos + external_deps_infos,
)
- # Compile sources
- _, compilation_outputs = cc_common.compile(
- name = ctx.label.name,
- actions = ctx.actions,
- feature_configuration = feature_configuration,
- cc_toolchain = cc_toolchain,
- srcs = ctx.files.srcs,
- compilation_contexts = [all_deps_cc_info.compilation_context],
- )
-
# Static deps are linked directly into the .so
- static_linking_context = cc_common.merge_cc_infos(
+ static_cc_info = cc_common.merge_cc_infos(
cc_infos = static_deps_infos,
- ).linking_context
+ )
# Dynamic deps are linked as shared libraries
dynamic_linking_context = cc_common.merge_cc_infos(
cc_infos = dynamic_deps_infos,
).linking_context
- # For external deps, we need to allow undefined symbols.
user_link_flags = []
+ user_link_flags.append("-Wl,--export-dynamic-symbol=PyInit_{module_name}".format(
+ module_name = module_name,
+ ))
+
+ # The PyInit symbol looks unused, so the linker optimizes it away. Telling it
+ # to treat it as undefined causes it to be retained.
+ user_link_flags.append("-Wl,--undefined=PyInit_{module_name}".format(
+ module_name = module_name,
+ ))
+
if ctx.attr.external_deps:
user_link_flags.append("-Wl,--allow-shlib-undefined")
- # This function also does the linking
- _, linking_outputs = cc_common.create_linking_context_from_compilation_outputs(
- name = ctx.label.name,
+ # todo: use toolchain to determine `abi3.` infix
+ # todo: use toolchain to determine platform extension (pyd, so, etc)
+ output_filename = "{module_name}.{ext}".format(
+ module_name = module_name,
+ ext = "so",
+ )
+ py_dso = ctx.actions.declare_file(output_filename)
+
+ static_linking_context = static_cc_info.linking_context
+ linking_contexts = [
+ static_linking_context,
+ dynamic_linking_context,
+ ]
+
+ # Add target-level linkopts last so users can override.
+ user_link_flags.extend(ctx.attr.linkopts)
+ print((
+ "===LINK:\n" +
+ " user_link_flags={user_link_flags}"
+ ).format(
+ user_link_flags = user_link_flags,
+ ))
+
+ # todo: add linker script to hide symbols by default
+ # py_internal allows using some private apis, which may or may not be needed.
+ # based upon cc_shared_library.bzl
+ cc_linking_outputs = py_internal.link(
actions = ctx.actions,
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
- compilation_outputs = compilation_outputs,
+ linking_contexts = linking_contexts,
user_link_flags = user_link_flags,
- linking_contexts = [static_linking_context, dynamic_linking_context],
+ # todo: add additional_inputs
+ name = ctx.label.name,
+ output_type = "dynamic_library",
+ main_output = py_dso,
+ # todo: maybe variables_extension
+ # todo: maybe additional_outputs
)
-
- print(linking_outputs)
- ltl = linking_outputs.library_to_link
- print(ltl)
- print(ltl.dynamic_library)
- print(ltl.resolved_symlink_dynamic_library)
- lib_dso = ltl.resolved_symlink_dynamic_library
- if lib_dso == None:
- lib_dso = ltl.dynamic_library
-
- if lib_dso == None:
- fail("No DSO output found in {}".format(ltl))
-
- # todo: pick appropriate infix based on py_extension attr settings
- py_dso = ctx.actions.declare_file("{}.so".format(ctx.label.name))
- ctx.actions.run_shell(
- command = 'cp "$1" "$2"',
- arguments = [lib_dso.path, py_dso.path],
- inputs = [lib_dso],
- outputs = [py_dso],
- )
+ print((
+ "===LINK OUTPUT:\n" +
+ " {}"
+ ).format(
+ cc_linking_outputs,
+ ))
# Propagate CcInfo from dynamic and external deps, but not static ones.
propagated_cc_info = cc_common.merge_cc_infos(
@@ -87,10 +102,7 @@
)
return [
- DefaultInfo(
- files = depset([py_dso]),
- runfiles = ctx.runfiles([py_dso]),
- ),
+ DefaultInfo(files = depset([py_dso])),
PyInfo(
transitive_sources = depset([py_dso]),
),
@@ -110,15 +122,14 @@
doc = "cc_library targets with external linkage.",
default = [],
),
- "srcs": lambda: attrb.LabelList(
- allow_files = True,
- doc = "The list of source files that are processed to create the target.",
- ),
"static_deps": lambda: attrb.LabelList(
providers = [CcInfo],
doc = "cc_library targets to be statically and privately linked.",
default = [],
),
+ "copts": lambda: attrb.StringList(),
+ "linkopts": lambda: attrb.StringList(),
+ "module_name": lambda: attrb.String(),
}
def create_py_extension_rule_builder(**kwargs):
diff --git a/tests/cc/py_extension/BUILD.bazel b/tests/cc/py_extension/BUILD.bazel
index 6b04ca9..b68dbef 100644
--- a/tests/cc/py_extension/BUILD.bazel
+++ b/tests/cc/py_extension/BUILD.bazel
@@ -14,14 +14,35 @@
py_extension(
name = "ext_static",
- srcs = ["ext_static.c"],
+ ##srcs = ["ext_static.c"],
static_deps = [":static_dep"],
)
py_extension(
name = "ext_shared",
+ dynamic_deps = [
+ ":add_one",
+ ],
+ static_deps = [
+ ":ext_shared_impl",
+ ],
+)
+
+cc_library(
+ name = "ext_shared_impl",
srcs = ["ext_shared.c"],
- dynamic_deps = [":dyn_dep_a"],
+ copts = [
+ # Gemini says PIC is needed
+ "-fPIC",
+ "-fvisibility=hidden",
+ ],
+ deps = [
+ ":add_one_headers",
+ # todo: if we put this here, we statically link add_one into the
+ # extension.
+ #":add_one_impl",
+ "@rules_python//python/cc:current_py_cc_headers",
+ ],
)
cc_library(
@@ -31,10 +52,13 @@
)
cc_library(
- name = "dyn_dep_a",
- srcs = ["dyn_dep_a.c"],
- hdrs = ["dyn_dep_a.h"],
- linkstatic = False,
+ name = "add_one_headers",
+ hdrs = ["add_one.h"],
+)
+
+cc_library(
+ name = "add_one",
+ srcs = ["add_one.c"],
)
py_test(
diff --git a/tests/cc/py_extension/add_one.c b/tests/cc/py_extension/add_one.c
new file mode 100644
index 0000000..c83da4e
--- /dev/null
+++ b/tests/cc/py_extension/add_one.c
@@ -0,0 +1,3 @@
+int add_one(int x) {
+ return x + 1;
+}
diff --git a/tests/cc/py_extension/add_one.h b/tests/cc/py_extension/add_one.h
new file mode 100644
index 0000000..eda0abf
--- /dev/null
+++ b/tests/cc/py_extension/add_one.h
@@ -0,0 +1,6 @@
+#ifndef TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_
+#define TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_
+
+int add_one(int x);
+
+#endif // TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_
diff --git a/tests/cc/py_extension/dyn_dep_a.c b/tests/cc/py_extension/dyn_dep_a.c
deleted file mode 100644
index e69de29..0000000
--- a/tests/cc/py_extension/dyn_dep_a.c
+++ /dev/null
diff --git a/tests/cc/py_extension/dyn_dep_a.h b/tests/cc/py_extension/dyn_dep_a.h
deleted file mode 100644
index e69de29..0000000
--- a/tests/cc/py_extension/dyn_dep_a.h
+++ /dev/null
diff --git a/tests/cc/py_extension/ext_shared.c b/tests/cc/py_extension/ext_shared.c
index 0aacba5..4b79a6d 100644
--- a/tests/cc/py_extension/ext_shared.c
+++ b/tests/cc/py_extension/ext_shared.c
@@ -1 +1,33 @@
-/* A no-op C extension for shared linking tests. */
+#include <Python.h>
+
+#include "tests/cc/py_extension/add_one.h"
+
+// A simple function that returns a Python integer.
+static PyObject* do_alpha(PyObject* self, PyObject* args) {
+ return PyLong_FromLong(add_one(41));
+}
+
+// Method definition object for this extension, these are the functions
+// that will be available in the module.
+static PyMethodDef ModuleMethods[] = {
+ {"do_alpha", do_alpha, METH_NOARGS, "A simple C function."},
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+// Module definition
+// The arguments of this structure tell Python what to call your extension,
+// what its methods are and where to look for its method definitions.
+static struct PyModuleDef ext_shared_module = {
+ PyModuleDef_HEAD_INIT,
+ "ext_shared", /* name of module */
+ NULL, /* module documentation, may be NULL */
+ -1, /* size of per-interpreter state of the module,
+ or -1 if the module keeps state in global variables. */
+ ModuleMethods
+};
+
+// The module init function. This must be exported and retained in the
+// shared library output.
+PyMODINIT_FUNC PyInit_ext_shared(void) {
+ return PyModule_Create(&ext_shared_module);
+}
diff --git a/tests/cc/py_extension/py_extension_test.py b/tests/cc/py_extension/py_extension_test.py
index 9a19e6a..81f277f 100644
--- a/tests/cc/py_extension/py_extension_test.py
+++ b/tests/cc/py_extension/py_extension_test.py
@@ -1,41 +1,36 @@
-import os
import unittest
-
-from elftools.elf.dynamic import DynamicSection
-from elftools.elf.elffile import ELFFile
-
+import os
from python.runfiles import runfiles
-
+from elftools.elf.elffile import ELFFile
+from elftools.elf.dynamic import DynamicSection
+import ext_shared
class PyExtensionTest(unittest.TestCase):
def test_inspect_elf(self):
r = runfiles.Create()
ext_path = r.Rlocation("rules_python/tests/cc/py_extension/ext_shared.so")
- self.assertTrue(
- os.path.exists(ext_path), f"Could not find ext_shared.so at {ext_path}"
- )
+ self.assertTrue(os.path.exists(ext_path), f"Could not find ext_shared.so at {ext_path}")
- with open(ext_path, "rb") as f:
+ with open(ext_path, 'rb') as f:
elf = ELFFile(f)
# Check for DT_NEEDED entry for the dynamic library
- dynamic_section = elf.get_section_by_name(".dynamic")
+ dynamic_section = elf.get_section_by_name('.dynamic')
self.assertIsNotNone(dynamic_section)
self.assertTrue(isinstance(dynamic_section, DynamicSection))
- needed_libs = [
- tag.needed
- for tag in dynamic_section.iter_tags()
- if tag.entry.d_tag == "DT_NEEDED"
- ]
- self.assertIn("libdyn_dep_a.so", needed_libs)
+ needed_libs = [tag.needed for tag in dynamic_section.iter_tags() if tag.entry.d_tag == 'DT_NEEDED']
+ self.assertIn('libdyn_dep_a.so', needed_libs)
# Check for the PyInit symbol
- dynsym_section = elf.get_section_by_name(".dynsym")
+ dynsym_section = elf.get_section_by_name('.dynsym')
self.assertIsNotNone(dynsym_section)
symbols = [s.name for s in dynsym_section.iter_symbols()]
- self.assertIn("PyInit_ext_shared", symbols)
+ self.assertIn('PyInit_ext_shared', symbols)
+
+ def test_import_and_call(self):
+ self.assertEqual(ext_shared.my_c_function(), 42)
if __name__ == "__main__":