diff --git a/CMakeLists.txt b/CMakeLists.txt index 7916fe784..226ee08b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,12 @@ endif() include(GNUInstallDirs) option(BUILD_NAJA_PYTHON "Build Naja Python package" OFF) +set(NAJA_EMBEDDED_LIBRARY_TYPE STATIC) +if(BUILD_NAJA_PYTHON AND WIN32) + # Put these objects in their owning DLL's input list so CMake's automatic + # export generation includes SDK entry points from the embedded components. + set(NAJA_EMBEDDED_LIBRARY_TYPE OBJECT) +endif() if(BUILD_NAJA_PYTHON AND WIN32) # Make imported targets from find_package() globally visible so parent # directories can consume fmt exported by thirdparty/slang. diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 7f0815828..6bef0b31e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -54,7 +54,7 @@ set(NAJA_CORE_HEADERS list(APPEND NAJA_CORE_HEADERS ${CMAKE_CURRENT_BINARY_DIR}/NajaVersion.h) -add_library(naja_core STATIC ${SOURCES}) +add_library(naja_core ${NAJA_EMBEDDED_LIBRARY_TYPE} ${SOURCES}) set_property(TARGET naja_core PROPERTY POSITION_INDEPENDENT_CODE ON) #target_link_libraries(naja_core PUBLIC cpptrace::cpptrace coverage_config sanitizers_config) target_link_libraries(naja_core PUBLIC coverage_config sanitizers_config naja_fmt) diff --git a/src/najaeda/CMakeLists.txt b/src/najaeda/CMakeLists.txt index 2fd942867..a9bceb6a8 100644 --- a/src/najaeda/CMakeLists.txt +++ b/src/najaeda/CMakeLists.txt @@ -3,6 +3,24 @@ # SPDX-License-Identifier: Apache-2.0 if(BUILD_NAJA_PYTHON) + # Ship the headers matching this wheel's libraries so native consumers can + # link the installed runtime instead of building a second copy of Naja. + install(DIRECTORY ${PROJECT_SOURCE_DIR}/src/ + DESTINATION najaeda/sdk/include/naja + FILES_MATCHING PATTERN "*.h") + install(FILES ${PROJECT_BINARY_DIR}/src/core/NajaVersion.h + DESTINATION najaeda/sdk/include/naja/core) + install(FILES + ${PROJECT_SOURCE_DIR}/thirdparty/naja-verilog/src/VerilogConstructor.h + ${PROJECT_SOURCE_DIR}/thirdparty/naja-verilog/src/VerilogTypes.h + DESTINATION najaeda/sdk/include/naja/verilog) + install(DIRECTORY ${SPDLOG_DIR}/spdlog ${ARGPARSE_DIR}/argparse + DESTINATION najaeda/sdk/include) + if(WIN32) + foreach(runtime IN ITEMS naja_nl naja_dnl naja_bne naja_opt naja_metrics naja_python) + install(FILES "$" DESTINATION najaeda/sdk/lib) + endforeach() + endif() install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/najaeda DESTINATION . PATTERN "*~" EXCLUDE diff --git a/src/nl/formats/liberty/CMakeLists.txt b/src/nl/formats/liberty/CMakeLists.txt index 1ddae7f66..646924b43 100644 --- a/src/nl/formats/liberty/CMakeLists.txt +++ b/src/nl/formats/liberty/CMakeLists.txt @@ -8,7 +8,7 @@ set(SOURCES SET(HEADERS SNLLibertyConstructor.h) -add_library(naja_snl_liberty STATIC ${SOURCES}) +add_library(naja_snl_liberty ${NAJA_EMBEDDED_LIBRARY_TYPE} ${SOURCES}) set_property(TARGET naja_snl_liberty PROPERTY POSITION_INDEPENDENT_CODE ON) target_link_libraries(naja_snl_liberty diff --git a/src/nl/formats/systemverilog/CMakeLists.txt b/src/nl/formats/systemverilog/CMakeLists.txt index 09388829c..d59e12ae7 100644 --- a/src/nl/formats/systemverilog/CMakeLists.txt +++ b/src/nl/formats/systemverilog/CMakeLists.txt @@ -13,7 +13,7 @@ set(HEADERS frontend/SNLSVIntent.h ) -add_library(naja_snl_systemverilog STATIC ${SOURCES}) +add_library(naja_snl_systemverilog ${NAJA_EMBEDDED_LIBRARY_TYPE} ${SOURCES}) set_property(TARGET naja_snl_systemverilog PROPERTY POSITION_INDEPENDENT_CODE ON) add_library(naja_snl_systemverilog_test STATIC EXCLUDE_FROM_ALL ${SOURCES}) diff --git a/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp b/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp index 05120ec70..778b38601 100644 --- a/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp +++ b/src/nl/formats/systemverilog/frontend/SNLSVConstructor.cpp @@ -7450,25 +7450,60 @@ endmodule } } - for (const auto* seqBlock : sequentialBlocks) { - const Statement* candidateStmt = &seqBlock->getBody(); + // An inferred memory is owned by one sequential block. A symbol written + // from several blocks (for example one always block per element in a + // generate loop) would leave the other writers to the generic lowering, + // which then drives the memory's read data a second time. Such a symbol + // is left to the generic lowering for all of its writers. + const auto collectDirectCandidates = + [&](const slang::ast::ProceduralBlockSymbol& seqBlock, + std::vector& candidates) { + const Statement* candidateStmt = &seqBlock.getBody(); if (const auto* timed = findTimedStatement(*candidateStmt)) { candidateStmt = &timed->stmt; } candidateStmt = candidateStmt ? unwrapStatement(*candidateStmt) : nullptr; if (!candidateStmt) { - continue; // LCOV_EXCL_LINE + return; // LCOV_EXCL_LINE } - std::vector candidates; std::unordered_set seenCandidates; collectDirectSequentialMemoryCandidates( *candidateStmt, candidates, seenCandidates); + }; + std::unordered_map directWriterBlocks; + for (const auto* seqBlock : sequentialBlocks) { + std::vector candidates; + collectDirectCandidates(*seqBlock, candidates); + for (const auto* stateSymbol : candidates) { + if (stateSymbol) { + ++directWriterBlocks[stateSymbol]; + } + } + } + + for (const auto* seqBlock : sequentialBlocks) { + std::vector candidates; + collectDirectCandidates(*seqBlock, candidates); for (const auto* stateSymbol : candidates) { if (!stateSymbol || inferredMemoryByStateSymbol_.contains(stateSymbol)) { continue; } + if (directWriterBlocks[stateSymbol] > 1) { + if (warnedUninferredMemorySymbols_.insert(stateSymbol).second) { + std::ostringstream reason; + reason << "Memory '" << std::string(stateSymbol->name) + << "' was not inferred as naja_mem: written from " + << directWriterBlocks[stateSymbol] + << " sequential blocks; using generic sequential lowering"; + reportWarning( + "uninferred_memory_generic_sequential_lowering", + reason.str(), + getSourceRange(*stateSymbol)); + } + continue; + } InferredMemory memory; std::string failureReason; if (!tryMatchDirectSequentialMemoryBlock( diff --git a/src/nl/formats/verilog/CMakeLists.txt b/src/nl/formats/verilog/CMakeLists.txt index a0cac8811..b82c804d9 100644 --- a/src/nl/formats/verilog/CMakeLists.txt +++ b/src/nl/formats/verilog/CMakeLists.txt @@ -14,7 +14,7 @@ SET(HEADERS backend/SNLVRLDumper.h ) -add_library(naja_snl_verilog STATIC ${SOURCES}) +add_library(naja_snl_verilog ${NAJA_EMBEDDED_LIBRARY_TYPE} ${SOURCES}) set_property(TARGET naja_snl_verilog PROPERTY POSITION_INDEPENDENT_CODE ON) target_link_libraries(naja_snl_verilog naja_verilog naja_nl) diff --git a/src/nl/netlist/serialization/capnp/CMakeLists.txt b/src/nl/netlist/serialization/capnp/CMakeLists.txt index f10ea16c0..2f400cba6 100644 --- a/src/nl/netlist/serialization/capnp/CMakeLists.txt +++ b/src/nl/netlist/serialization/capnp/CMakeLists.txt @@ -9,7 +9,7 @@ capnp_generate_cpp(najaCommonSources najaCommonHeaders ${CAPNPC_SRC_PREFIX}/naja capnp_generate_cpp(najaNLInterfaceSources najaNLInterfaceHeaders ${CAPNPC_SRC_PREFIX}/naja_nl_interface.capnp) capnp_generate_cpp(najaNLImplementationSources najaNLImplementationHeaders ${CAPNPC_SRC_PREFIX}/naja_nl_implementation.capnp) -add_library(naja_nl_dump STATIC +add_library(naja_nl_dump ${NAJA_EMBEDDED_LIBRARY_TYPE} ${najaCommonSources} ${najaNLInterfaceSources} ${najaNLImplementationSources} SNLDump.cpp SNLDumpManifest.cpp diff --git a/src/nl/netlist/snl/SNLInstParameter.cpp b/src/nl/netlist/snl/SNLInstParameter.cpp index 77cc6c18e..fd3e14748 100644 --- a/src/nl/netlist/snl/SNLInstParameter.cpp +++ b/src/nl/netlist/snl/SNLInstParameter.cpp @@ -50,6 +50,9 @@ void SNLInstParameter::preDestroy() { } void SNLInstParameter::destroyFromInstance() { + // The owning instance is already clearing its parameter collection. Still + // release properties so retained Python wrappers are invalidated. + super::preDestroy(); delete this; } diff --git a/src/nl/netlist/snl/SNLParameter.cpp b/src/nl/netlist/snl/SNLParameter.cpp index 390a5a7b1..f93d42deb 100644 --- a/src/nl/netlist/snl/SNLParameter.cpp +++ b/src/nl/netlist/snl/SNLParameter.cpp @@ -66,10 +66,14 @@ void SNLParameter::preCreate(SNLDesign* design, const NLName& name) { void SNLParameter::destroy() { design_->removeParameter(this); + NajaObject::preDestroy(); delete this; } void SNLParameter::destroyFromDesign() { + // Notify property owners before freeing the parameter, including Python + // wrappers that may outlive the design or universe. + NajaObject::preDestroy(); delete this; } @@ -107,4 +111,4 @@ std::string SNLParameter::getDescription() const { } //LCOV_EXCL_STOP -} // namespace naja::NL \ No newline at end of file +} // namespace naja::NL diff --git a/src/nl/netlist/visual/CMakeLists.txt b/src/nl/netlist/visual/CMakeLists.txt index 2226d2aa5..01403d8b4 100644 --- a/src/nl/netlist/visual/CMakeLists.txt +++ b/src/nl/netlist/visual/CMakeLists.txt @@ -9,7 +9,7 @@ set(NETLIST_GRAPH_HEADERS NetlistGraph.h NetlistGraph_impl.h ) -add_library(naja_snl_visual STATIC ${NETLIST_GRAPH_SOURCES}) +add_library(naja_snl_visual ${NAJA_EMBEDDED_LIBRARY_TYPE} ${NETLIST_GRAPH_SOURCES}) set_property(TARGET naja_snl_visual PROPERTY POSITION_INDEPENDENT_CODE ON) target_link_libraries(naja_snl_visual PUBLIC naja_nl) diff --git a/test/nl/python/naja_wrapping/test_parameter_lifetime.py b/test/nl/python/naja_wrapping/test_parameter_lifetime.py new file mode 100644 index 000000000..f0e9cee0a --- /dev/null +++ b/test/nl/python/naja_wrapping/test_parameter_lifetime.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2026 The Naja authors +# SPDX-License-Identifier: Apache-2.0 + +import gc +import unittest + +import naja + + +class ParameterLifetimeTests(unittest.TestCase): + def setUp(self): + self.universe = naja.NLUniverse.create() + library = naja.NLLibrary.create(naja.NLDB.create(self.universe)) + self.model = naja.SNLDesign.create(library, "model") + self.parameter = naja.SNLParameter.create_string(self.model, "INIT", "x") + self.top = naja.SNLDesign.create(library, "top") + self.instance = naja.SNLInstance.create(self.top, self.model, "i") + self.inst_parameter = naja.SNLInstParameter.create( + self.instance, self.parameter, "y") + + def tearDown(self): + if naja.NLUniverse.get() is not None: + naja.NLUniverse.get().destroy() + + def test_universe_destruction_invalidates_retained_parameter_wrappers(self): + self.universe.destroy() + with self.assertRaisesRegex(RuntimeError, "unbound"): + self.parameter.getName() + with self.assertRaisesRegex(RuntimeError, "unbound"): + self.inst_parameter.getValue() + # Used to dereference freed objects and abort from the deallocator. + self.parameter = self.inst_parameter = None + gc.collect() + + def test_instance_destruction_invalidates_only_its_parameter_wrapper(self): + self.instance.destroy() + with self.assertRaisesRegex(RuntimeError, "unbound"): + self.inst_parameter.getValue() + self.assertEqual("INIT", self.parameter.getName()) + self.inst_parameter = None + gc.collect() + + def test_design_destruction_invalidates_parameter_wrapper(self): + self.instance.destroy() + self.model.destroy() + with self.assertRaisesRegex(RuntimeError, "unbound"): + self.parameter.getName() + + def test_explicit_parameter_destruction_invalidates_wrapper(self): + self.instance.destroy() + self.parameter.destroy() + with self.assertRaisesRegex(RuntimeError, "unbound"): + self.parameter.getName() + + +if __name__ == "__main__": + unittest.main()