From fb2226a29273015b1242899099b909f6bc6809f0 Mon Sep 17 00:00:00 2001 From: Chris Winstead <57018137+cjwinstead@users.noreply.github.com> Date: Sat, 2 Aug 2025 14:26:35 -0600 Subject: [PATCH 01/12] fixed missing parens in getphasevector --- src/API/get_vector.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/API/get_vector.jl b/src/API/get_vector.jl index 3995d8d..d026032 100644 --- a/src/API/get_vector.jl +++ b/src/API/get_vector.jl @@ -97,7 +97,7 @@ function getmagnitudevec(name, maxlen=Int(maxintfloat())) abs.(data) end -function getphasevec(name, maxlen=Int(maxintfloat)) +function getphasevec(name, maxlen=Int(maxintfloat())) _, __, data = get_vector_info(name, maxlen) angle.(data) end From 3e316fc37f7299861259f1ab3d5db9e611abdca1 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Thu, 14 May 2026 16:27:06 -0600 Subject: [PATCH 02/12] Disabled graph and repl features due to segfault. --- Manifest.toml | 134 ++++++++++++++++++++++++++++++++++++++----------- Project.toml | 4 +- src/NgSpice.jl | 4 +- 3 files changed, 110 insertions(+), 32 deletions(-) diff --git a/Manifest.toml b/Manifest.toml index f6d406d..2850ca6 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,12 +2,15 @@ [[ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" [[Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" [[Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" [[CEnum]] git-tree-sha1 = "215a9aa4a1f23fbd05b92769fdd62559488d70e9" @@ -26,13 +29,24 @@ git-tree-sha1 = "417b0ed7b8b838aa6ca0a87aadf1bb9eb111ce40" uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" version = "0.12.8" +[[CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.0+1" + [[Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" [[Downloads]] -deps = ["ArgTools", "LibCURL", "NetworkOptions"] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.7.0" + +[[FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" [[FixedPointNumbers]] deps = ["Statistics"] @@ -43,6 +57,7 @@ version = "0.8.4" [[InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" [[JLLWrappers]] deps = ["Preferences"] @@ -50,49 +65,98 @@ git-tree-sha1 = "642a199af8b68253517b80bd3bfd17eb4e84df6e" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" version = "1.3.0" +[[JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + [[LibCURL]] deps = ["LibCURL_jll", "MozillaCACerts_jll"] uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" [[LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.15.0+0" [[LibGit2]] -deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + +[[LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.9.0+0" [[LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.3+1" [[Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" [[LinearAlgebra]] -deps = ["Libdl"] +deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +version = "1.12.0" [[Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" [[Markdown]] -deps = ["Base64"] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" + +[[MbedTLS]] +deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] +git-tree-sha1 = "c067a280ddc25f196b5e7df3877c6b226d390aaf" +uuid = "739be429-bea8-5141-9913-cc70e7f3736d" +version = "1.1.9" [[MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "0eef589dd1c26a3ac9d753fe1a8bcad63f956fa6" uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.16.8+1" [[MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2025.11.4" [[NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.3.0" + +[[NgSpice]] +deps = ["CEnum", "Colors", "Libdl", "RecipesBase", "ReplMaker", "Requires", "ngspice_jll"] +path = "." +uuid = "1a64e563-3ad9-44a2-8713-a1728bea8a90" +version = "0.1.4" + +[[OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +version = "0.3.29+0" + +[[OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.4+0" [[Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.12.1" +weakdeps = ["REPL"] + + [Pkg.extensions] + REPLExt = "REPL" [[Preferences]] deps = ["TOML"] @@ -103,14 +167,17 @@ version = "1.2.1" [[Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" [[REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" [[Random]] -deps = ["Serialization"] +deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" [[RecipesBase]] git-tree-sha1 = "b3fb709f3c97bfc6e948be68beeecb55a0b340ae" @@ -136,53 +203,61 @@ version = "1.1.3" [[SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[SafeTestsets]] -deps = ["Test"] -git-tree-sha1 = "36ebc5622c82eb9324005cc75e7e2cc51181d181" -uuid = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" -version = "0.0.1" - -[[Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "0.7.0" [[Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[SparseArrays]] -deps = ["LinearAlgebra", "Random"] -uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +version = "1.11.0" [[Statistics]] -deps = ["LinearAlgebra", "SparseArrays"] +deps = ["LinearAlgebra"] +git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.11.1" + + [Statistics.extensions] + SparseArraysExt = ["SparseArrays"] + + [Statistics.weakdeps] + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" [[TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" [[Tar]] deps = ["ArgTools", "SHA"] uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" - -[[Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.10.0" [[UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" [[Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" [[Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.3.1+2" + +[[libblastrampoline_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +version = "5.15.0+0" [[nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.64.0+1" [[ngspice_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] @@ -193,5 +268,6 @@ uuid = "57e080ce-e004-5ece-a144-c95753423b2a" version = "34.0.0+1" [[p7zip_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.7.0+0" diff --git a/Project.toml b/Project.toml index af4a47a..782caa6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,12 +1,13 @@ name = "NgSpice" uuid = "1a64e563-3ad9-44a2-8713-a1728bea8a90" -authors = ["Venkateshprasad Bhat"] version = "0.1.4" +authors = ["Venkateshprasad Bhat"] [deps] CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" ReplMaker = "b873ce64-0db9-51f5-a568-4457d8e49576" Requires = "ae029012-a4dd-5104-9daa-d747884805df" @@ -15,6 +16,7 @@ ngspice_jll = "57e080ce-e004-5ece-a144-c95753423b2a" [compat] CEnum = "0.4, 0.5" Colors = "0.12" +MbedTLS = "1.1.9" RecipesBase = "1.1" ReplMaker = "0.2" Requires = "1.1" diff --git a/src/NgSpice.jl b/src/NgSpice.jl index b68b822..c243739 100644 --- a/src/NgSpice.jl +++ b/src/NgSpice.jl @@ -31,8 +31,8 @@ include("API/sim_utils.jl") include("API/running.jl") function __init__() - @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" @eval include("API/repl.jl") - @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" @eval include("API/graphs.jl") +# @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" @eval include("API/repl.jl") +# @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" @eval include("API/graphs.jl") # Ensure that NgSpice is initialized, as otherwise it's easy to get segfaults # because NgSpice expects a printf callback to be provided (as we do in the From c4fd77c7e352648acdaa42f029d9240eeb2517ab Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Thu, 4 Jun 2026 22:56:56 -0600 Subject: [PATCH 03/12] Major edits to fix (hopefully) the GC segfaults by using a CircularBuffer for callback messages. Removed some non-essential features and dependencies. Renamed fork to NgHerb. --- Manifest.toml | 210 ++++++-------------------------- Project.toml | 30 ++--- README.md | 98 +++++++++------ src/API/graphs.jl | 72 ----------- src/API/repl.jl | 29 ----- src/API/running.jl | 36 ++---- src/API/sim_utils.jl | 24 ++-- src/NgSpice.jl | 51 -------- src/extensions.jl | 30 ----- src/interface/callbacks.jl | 21 ++++ src/interface/ctypes.jl | 7 -- src/interface/ngspice_api.jl | 65 +++++----- src/interface/ngspice_common.jl | 87 +------------ test/ngspice_api.jl | 15 --- test/runtests.jl | 15 --- test/wrapper_api.jl | 34 ------ test/wrapper_safetests.jl | 16 --- tutorials/demo.jl | 12 -- tutorials/mosfet.jl | 21 ---- 19 files changed, 190 insertions(+), 683 deletions(-) delete mode 100644 src/API/graphs.jl delete mode 100644 src/API/repl.jl delete mode 100644 src/NgSpice.jl delete mode 100644 src/extensions.jl create mode 100644 src/interface/callbacks.jl delete mode 100644 src/interface/ctypes.jl delete mode 100644 test/ngspice_api.jl delete mode 100644 test/runtests.jl delete mode 100644 test/wrapper_api.jl delete mode 100644 test/wrapper_safetests.jl delete mode 100644 tutorials/demo.jl delete mode 100644 tutorials/mosfet.jl diff --git a/Manifest.toml b/Manifest.toml index f6d406d..ec4da10 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,197 +1,67 @@ # This file is machine-generated - editing it directly is not advised -[[ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +julia_version = "1.12.6" +manifest_format = "2.0" +project_hash = "f8556de8ceff5f13bdaf89939adbdadb5282f7ec" -[[Artifacts]] +[[deps.Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" -[[Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +[[deps.DataStructures]] +deps = ["OrderedCollections"] +git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.19.5" -[[CEnum]] -git-tree-sha1 = "215a9aa4a1f23fbd05b92769fdd62559488d70e9" -uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" -version = "0.4.1" - -[[ColorTypes]] -deps = ["FixedPointNumbers", "Random"] -git-tree-sha1 = "024fe24d83e4a5bf5fc80501a314ce0d1aa35597" -uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" -version = "0.11.0" - -[[Colors]] -deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] -git-tree-sha1 = "417b0ed7b8b838aa6ca0a87aadf1bb9eb111ce40" -uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" -version = "0.12.8" - -[[Dates]] +[[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" -[[Downloads]] -deps = ["ArgTools", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" - -[[FixedPointNumbers]] -deps = ["Statistics"] -git-tree-sha1 = "335bfdceacc84c5cdf16aadc768aa5ddfc5383cc" -uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" -version = "0.8.4" - -[[InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[JLLWrappers]] -deps = ["Preferences"] -git-tree-sha1 = "642a199af8b68253517b80bd3bfd17eb4e84df6e" +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.3.0" - -[[LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" - -[[LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" - -[[LibGit2]] -deps = ["Base64", "NetworkOptions", "Printf", "SHA"] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.8.0" -[[LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" - -[[Libdl]] +[[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" -[[LinearAlgebra]] -deps = ["Libdl"] -uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" - -[[Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - -[[Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +[[deps.NgHerb]] +deps = ["DataStructures", "ngspice_jll"] +path = "." +uuid = "16d751f2-2168-46b0-9d00-9e1470832ba3" +version = "0.2.1" -[[MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +[[deps.OrderedCollections]] +git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.8.2" -[[NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" - -[[Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" - -[[Preferences]] +[[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "ea79e4c9077208cd3bc5d29631a26bc0cff78902" +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.2.1" +version = "1.5.2" -[[Printf]] +[[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" -[[REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" - -[[Random]] -deps = ["Serialization"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[[RecipesBase]] -git-tree-sha1 = "b3fb709f3c97bfc6e948be68beeecb55a0b340ae" -uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" -version = "1.1.1" - -[[Reexport]] -git-tree-sha1 = "57d8440b0c7d98fc4f889e478e80f268d534c9d5" -uuid = "189a3867-3050-52da-a836-e630ba90ab69" -version = "1.0.0" - -[[ReplMaker]] -deps = ["REPL", "Unicode"] -git-tree-sha1 = "76098218397ec93b925b70ce355144d539b1a8b4" -uuid = "b873ce64-0db9-51f5-a568-4457d8e49576" -version = "0.2.5" - -[[Requires]] -deps = ["UUIDs"] -git-tree-sha1 = "4036a3bd08ac7e968e27c203d45f5fff15020621" -uuid = "ae029012-a4dd-5104-9daa-d747884805df" -version = "1.1.3" - -[[SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[SafeTestsets]] -deps = ["Test"] -git-tree-sha1 = "36ebc5622c82eb9324005cc75e7e2cc51181d181" -uuid = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" -version = "0.0.1" - -[[Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[SparseArrays]] -deps = ["LinearAlgebra", "Random"] -uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" - -[[Statistics]] -deps = ["LinearAlgebra", "SparseArrays"] -uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" - -[[TOML]] +[[deps.TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" -[[Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" - -[[Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" - -[[Unicode]] +[[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" -[[Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" - -[[nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" - -[[ngspice_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "02d25c1bad237d00e23208770c48da1d46f68de1" -repo-rev = "main" -repo-url = "https://github.com/JuliaBinaryWrappers/ngspice_jll.jl" +[[deps.ngspice_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "58c3dc3bb44dd7e74086c6dfc9fcd69b732b5162" uuid = "57e080ce-e004-5ece-a144-c95753423b2a" -version = "34.0.0+1" - -[[p7zip_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "41.0.0+2" diff --git a/Project.toml b/Project.toml index af4a47a..e474b95 100644 --- a/Project.toml +++ b/Project.toml @@ -1,28 +1,12 @@ -name = "NgSpice" -uuid = "1a64e563-3ad9-44a2-8713-a1728bea8a90" -authors = ["Venkateshprasad Bhat"] -version = "0.1.4" +name = "NgHerb" +uuid = "16d751f2-2168-46b0-9d00-9e1470832ba3" +version = "0.2.1" +authors = ["Chris Winstead", "Venkateshprasad Bhat"] [deps] -CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" -Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" -Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" -ReplMaker = "b873ce64-0db9-51f5-a568-4457d8e49576" -Requires = "ae029012-a4dd-5104-9daa-d747884805df" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" ngspice_jll = "57e080ce-e004-5ece-a144-c95753423b2a" [compat] -CEnum = "0.4, 0.5" -Colors = "0.12" -RecipesBase = "1.1" -ReplMaker = "0.2" -Requires = "1.1" -julia = "1.5" - -[extras] -SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Test", "SafeTestsets"] +DataStructures = "0.19.5" +ngspice_jll = "41.0.0" diff --git a/README.md b/README.md index bf4c8f5..3dcdc3a 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,84 @@ -# NgSpice +# NgHerb -- A Revision of NgSpice -[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaComputing.github.io/NgSpice.jl/stable) -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaComputing.github.io/NgSpice.jl/dev) -[![Build Status](https://github.com/JuliaComputing/NgSpice.jl/workflows/CI/badge.svg)](https://github.com/JuliaComputing/NgSpice.jl/actions) -[![Coverage](https://codecov.io/gh/JuliaComputing/NgSpice.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaComputing/NgSpice.jl) +This repository provides a Julia wrapper for NgSpice library. It's based on the +[NgSpice](https://github.com/JuliaComputing/NgSpice.jl) Julia module by Venkateshprasad Bhat. +NgHerb implements these changes: + +* Thread safety for NgSpice callbacks, to resolve segfaults in Julia 12.6+ +* Removed unused callbacks +* Removed REPL and Plot features for easier maintenance + +NgHerb has some additional streamlining compared to NgSpice. + +The main purpose is to provide a minimal `ngspice` library wrapper that works stably with more recent +Julia versions. As of Julia 12.6, the original NgSpice module is broken unless +Julia is constrained to one thread, i.e. `julia -t 1`. NgHerb tries to fix this by +introducing a CircularBuffer to manage asyncrhonous output from the `ngspice`. -This repository provides a Julia wrapper for NgSpice library. ## Usage: In a Julia REPL, ``` -] add https://github.com/JuliaComputing/NgSpice.jl -using NgSpice +] add https://github.com/cjwinstead/NgHerb.jl +using NgHerb ``` --- -To start an an interactive mode, +Upon initialization, you shold see a startup message from the `ngspice` library, something +like this: + ``` -using Plots -NgSpice.interactive() +****** +** ngspice-46 shared library +** Creation Date: Tue May 12 23:05:42 UTC 2026 +****** ``` -- Hit `~` for initializing the NgSpice +### Basic Functions -Run the Ngspice simulations commands!
-For example: -``` -source /filepath/netlist # without quotes -display # prints all vectors and constants -plot vector1 vector2 # plots the real part of vector by default -``` +To load a netlist and run analyses in `ngspice`, use: +* `NgHerb.load_netlist(String)` -- loads a netlist from a multi-line Julia String +* `NgHerb.cmd(String)` -- run an `ngspice` command. -*Note*: It is not necessary for `.cir` to be placed in `bin` folder as long as full path is specified. +To retrieve data from `ngspice`, use: -Additional to usual plot parameters, different modes of retrieval can be set with:
- `plot -x space seperated vectorlist`
- where `x` takes following modes: +* `NgHerb.getrealvec(String)` -- get the named vector +* `NgHerb.getmagnitudevec(String)` -- get the magnitude of a complex vector +* `NgHerb.getphasevec(String)` -- get the phase of a complex vector (radians) -| Modes | Description | -|---------|-------| -| -r, --real | Real part of vector| -| -i, --imaginary | Imaginary part of vector | -| -m, --magnitude | Magnitude of vector | -| -p, --phase | Phase of vector | ---- -For a non-interactive and more Julia-like experience checkout this [tutorial](tutorials\mosfet.jl). +```julia-repl +julia> netlist=""" +* Demo circuit + +V1 1 0 DC 1 +R1 1 2 1k +R2 2 0 2k + +.end +"""; + +julia> NgHerb.load_netlist(netlist) +Circuit: * Demo circuit + +julia> NgHerb.cmd("op") + +Doing analysis at TEMP = 27.000000 and TNOM = 27.000000 +Using SPARSE 1.3 as Direct Linear Solver +No. of Data Rows : 1 + +julia> NgHerb.cmd("print all"); + +v(1) = 1.000000e+00 +v(2) = 6.666667e-01 +v1#branch = -3.33333e-04 + +julia> NgHerb.getrealvec("2") +1-element Vector{Float64}: + 0.6666666666666666 -Additionally, to simulate a `complex_circuit.sp` file with multiple plotting statements, run -``` -using NgSpice -using Plots -source_sp("path/to/the/complex_circuit.sp") ``` + diff --git a/src/API/graphs.jl b/src/API/graphs.jl deleted file mode 100644 index 49b020f..0000000 --- a/src/API/graphs.jl +++ /dev/null @@ -1,72 +0,0 @@ -using RecipesBase -using Colors -using .Plots - -struct NgSpiceGraphs end - -const graph = NgSpiceGraphs() - -# `fort` is frequency or time vector. It is the `X` axis and should always be real. -@recipe function ng(::NgSpiceGraphs, vectype, fort, veclist, title, xlims=nothing) - ft = getrealvec(fort) - label := permutedims(veclist) - grid := true - title := "$title" - background_color --> Colors.RGB(0.0, 0.0, 0.0) - xguide := fort - guide := length(veclist) > 1 ? "signals" : "signal" - color_palette --> :default - seriestype := :path - overwrite_figure := false - vec = vectype.(veclist) - #isa(xlims, Nothing) || xlims := xlims - ft, vec -end - -# source function for sp files needs to specially process plot commands -function source_sp(netpath) - netlines = readlines(netpath) - circpath = joinpath(pwd(), "net.cir") - for l in range(1, stop=length(netlines)) - if netlines[l] == ".control" - open(circpath, "a") do f - for line in netlines[1:l-1] - write(f, line*"\n") - end - end - try source(circpath) catch; rm(circpath) end - temp = l - while netlines[temp+1] != ".endc" - temp += 1 - try - if occursin("plot", netlines[temp]) - _plotswitch(netlines[temp]) - else - cmd(netlines[temp]) - end - catch - isfile(circpath) && rm(circpath) - end - end - end - end - isfile(circpath) && rm(circpath) -end - -function _plotswitch(pltstr) - params = split(pltstr[6:end], " ") - xlims = nothing - xlim_index = findall(occursin.("xlim", params)) - length(xlim_index) == 0 || (xlim_index = xlim_index[1]; xlims = parse.(Float64, (params[xlim_index+1], params[xlim_index+2])); - deleteat!(params, xlim_index:1:xlim_index+2)) - fort = "frequency" ∈ listcurvecs() ? "frequency" : "time" - pushfirst!(params, fort) - if !occursin("-", params[1]) plt = plot(graph, getrealvec, params[1], params[2:end], "Real plot", xlims) - elseif params[1] ∈ ("--real", "-r") plt = plot(graph, getrealvec, params[2], params[3:end], "Real plot", xlims) - elseif params[1] ∈ ("--imaginary", "-i") plt = plot(graph, getimagvec, params[2], params[3:end], "Imaginary plot", xlims) - elseif params[1] ∈ ("--magnitude", "-m") plt = plot(graph, getmagnitudevec, params[2], params[3:end], "Magnitude plot", xlims) - elseif params[1] ∈ ("--phase", "-p") plt = plot(graph, getphasevec, params[2], params[3:end], "Phase plot", xlims) - else throw("$(params[1]) is not a valid plot type") - end - Base.display(plt) -end diff --git a/src/API/repl.jl b/src/API/repl.jl deleted file mode 100644 index ec310e0..0000000 --- a/src/API/repl.jl +++ /dev/null @@ -1,29 +0,0 @@ -using ReplMaker -using .Plots - -function _ngspice_parser(str) - if str == "init" init() - elseif occursin("plot", str) - _plotswitch(str) - else - cmd(str) - end -end - -function _repl_parser(str) - if occursin(".sp", str) && str[1:7] == "source " - source_sp(str[8:end]) - else - _ngspice_parser(str) - end -end - -interactive() = (init(); - initrepl(_repl_parser, - prompt_text = "NgSpice> ", - prompt_color = :yellow, - start_key = '~', - mode_name = "for NgSpice is", - )) - - \ No newline at end of file diff --git a/src/API/running.jl b/src/API/running.jl index 75237b4..aed8159 100644 --- a/src/API/running.jl +++ b/src/API/running.jl @@ -1,34 +1,14 @@ -cmd(command) = ngSpice_Command(command) +cmd(command) = GC.@preserve command ngSpice_Command(command) +precompile(cmd,(String,)) function init() - pvoid = convert(Ptr{Nothing}, 0) - ngSpice_Init(gen_psendchar(), gen_psendstat(), - gen_pcontrolledexit(), - gen_psenddata(), - gen_psendinitdata(), - gen_pbgthread(), pvoid) - ngSpice_Init_JLExtensions( - @cfunction(path_resolve, Cstring, (Cstring, Cstring)) - ) + #pvoid = convert(Ptr{Nothing}, 0) + ngSpice_Init(gen_psendchar[], gen_psendstat[], + gen_pcontrolledexit[], + gen_psenddata[], + gen_psendinitdata[], + gen_pbgthread[], C_NULL) end -isrunning() = ngSpice_running() # always returns 0 -run() = (println("Running the simulator"); cmd("run")) -bgrun() = (println("Running the simulator in a background thread"); - cmd("bg_run")) - -set_breakpoint(bkpt::Float64) = ngSpice_SetBkpt(bkpt) - -stop() = (println("Stopping the simulator"); cmd("stop")) -bghalt() = (println("Halting the simulator in a background thread"); - cmd("bg_halt")) - -reset() = (println("Resetting the simulator"); cmd("reset")) -resume() = (println("Resuming the simulator"); cmd("resume")) - -alter(command::String) = cmd(string("alter ", command)) - -quit() = cmd("quit") -exit() = (println("Quitting immediately"); cmd("unset askquit"); cmd("quit")) diff --git a/src/API/sim_utils.jl b/src/API/sim_utils.jl index 8db6c14..50d1607 100644 --- a/src/API/sim_utils.jl +++ b/src/API/sim_utils.jl @@ -1,15 +1,19 @@ -function load_netlist(netlist::AbstractArray{T}) where T <: AbstractString - append!(netlist, ["C_NULL"]) - t = netlist |> ngSpice_Circ -end + function load_netlist(netlist::AbstractArray{T}) where T <: AbstractString + append!(netlist, ["C_NULL"]) + t = netlist |> ngSpice_Circ + end +# Opted to use the "circbyline" approach here to reduce +# memory crossover events between Julia and NgSpice. function load_netlist(netlist::AbstractString) - # The strings in Julia always use \n instead of \r \r\n - # (even if the editor does). - # But if the circuit-string has \r in it, - # uncomment the following line - # occursin("\r", netlist) && replace(netlist, "\r"=>"") - load_netlist(split(netlist, "\n")) + for line in eachsplit(netlist,"\n") + if contains(line,r"\w+") + cmd(string("circbyline ",line)) + end + end + if !contains(lowercase(netlist),r"\.end\s*$") + cmd("circbyline .end") + end end source(netpath) = cmd("source $netpath") diff --git a/src/NgSpice.jl b/src/NgSpice.jl deleted file mode 100644 index b68b822..0000000 --- a/src/NgSpice.jl +++ /dev/null @@ -1,51 +0,0 @@ -__precompile__() -module NgSpice - -using ngspice_jll -export ngspice_jll - -using Requires - -include("interface/ctypes.jl") -export Ctm, Ctime_t, Cclock_t - -include("interface/ngspice_common.jl") -include("interface/ngspice_api.jl") - -export bgthreadrunning, controlledexit, #getISRCdata, detsyncdata, getVSRCdata, - sendchar, senddata, sendstat, sendinitdata - -export gen_pbgthread, gen_pcontrolledexit, gen_psendchar, gen_psenddata, - gen_psendinitdata, gen_psendstat - -foreach(names(@__MODULE__, all=true)) do s - if startswith(string(s), "ng") - @eval export $s - end -end #should this be replaced with aliases - -include("extensions.jl") - -include("API/get_vector.jl") -include("API/sim_utils.jl") -include("API/running.jl") - -function __init__() - @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" @eval include("API/repl.jl") - @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" @eval include("API/graphs.jl") - - # Ensure that NgSpice is initialized, as otherwise it's easy to get segfaults - # because NgSpice expects a printf callback to be provided (as we do in the - # init() method we're calling here) - init() -end - -export get_vector_info, - curplot, listallplots, listallvecs, listcurvecs, getimagvec, - getmagnitudevec, getphasevec, getvec, getrealvec, - NgSpiceGraphs, graph, - load_netlist, source, source_sp, - bghalt, bgrun, cmd, init, - interactive, alter - #isrunning, -end diff --git a/src/extensions.jl b/src/extensions.jl deleted file mode 100644 index f5bfb8f..0000000 --- a/src/extensions.jl +++ /dev/null @@ -1,30 +0,0 @@ -const JLPATH_PREFIX = "jlpkg:" - -function c_strdup(str) - ptr = @ccall libngspice.tmalloc((sizeof(str)+1)::Cint)::Ptr{UInt8} - @GC.preserve str Base.unsafe_copyto!(ptr, pointer(str), sizeof(str)) - unsafe_store!(ptr, UInt8(0), sizeof(str) + 1) - ptr -end - -function path_resolve(name::Cstring, dir::Cstring)::Cstring - name = unsafe_string(name) - dir = unsafe_string(dir) - - if startswith(name, JLPATH_PREFIX) - path = name[sizeof(JLPATH_PREFIX)+1:end] - components = splitpath(path) - - @assert components[1] != "/" - - pkg_path = Base.locate_package(Base.PkgId(components[1])) - if pkg_path !== nothing - pkg_path = realpath(joinpath(dirname(pkg_path), "..")) - file_path = joinpath(pkg_path, components[2:end]...) - return c_strdup(file_path) - end - - return Ptr{Cchar}(-1) - end - return C_NULL -end diff --git a/src/interface/callbacks.jl b/src/interface/callbacks.jl new file mode 100644 index 0000000..399872e --- /dev/null +++ b/src/interface/callbacks.jl @@ -0,0 +1,21 @@ +function sendchar(_text::Ptr{Cchar}, id::Cint, userdata::Ptr{Cvoid})::Cint + copy_len = ccall(:strlen, Csize_t, (Ptr{Cchar},), _text) + for c in 1:copy_len + push!(string_buffer,unsafe_load(_text,c)) + end + push!(string_buffer,'\n') + + ccall(:uv_async_send, Cint, (Ptr{Cvoid},), async_cond[].handle) + return zero(Int32) +end + + + + +function controlledexit(exitstatus::Cint, immediate::Cint, quitexit::Cint, id::Cint, userdata::Ptr{Cvoid})::Cint + quitexit == 1 && println("Returned from quit with exit status") + return exitstatus +end + + + diff --git a/src/interface/ctypes.jl b/src/interface/ctypes.jl deleted file mode 100644 index 436e6ad..0000000 --- a/src/interface/ctypes.jl +++ /dev/null @@ -1,7 +0,0 @@ -## TODO: pending https://github.com/JuliaLang/julia/issues/29420 -# this one is suggested in the issue, but it looks like time_t and tm are two different things? -# const Ctime_t = Base.Libc.TmStruct - -const Ctm = Base.Libc.TmStruct -const Ctime_t = UInt -const Cclock_t = UInt diff --git a/src/interface/ngspice_api.jl b/src/interface/ngspice_api.jl index 8d7be04..562f0c8 100644 --- a/src/interface/ngspice_api.jl +++ b/src/interface/ngspice_api.jl @@ -2,36 +2,31 @@ function ngSpice_Init(printfcn, statfcn, ngexit, sdata, sinitdata, bgtrun, userD ccall((:ngSpice_Init, libngspice), Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), printfcn, statfcn, ngexit, sdata, sinitdata, bgtrun, userData) end -function ngSpice_Init_JLExtensions(path_resolve) - ret = try; - ccall((:ngSpice_Init_JLExtensions, libngspice), Cint, (Cint, Ptr{Cvoid},), - 1, path_resolve) - catch e; - @warn "Failed to initialize julia extensions. Some features may not work." - return false +#----------------------------------------------------------------- +# This implementation is really worried about thread safety, +# but maybe not altogether necessary? I assume the command +# string will be consumed rapidly by the receiving ccall +# and not needed again after it returns. +function ngSpice_Command(command::String) + # Calculate bytes needed (including the NUL terminator) + byte_count = sizeof(command) + 1 + + # 1. Allocate raw system memory that Julia's GC will completely ignore + c_buffer = Libc.malloc(byte_count) + + # 2. Safely copy Julia's string data into the raw system buffer + GC.@preserve command begin + src_ptr = Base.unsafe_convert(Cstring, command) + # Copy the characters + trailing NUL byte + unsafe_copyto!(convert(Ptr{UInt8}, c_buffer), convert(Ptr{UInt8}, src_ptr), byte_count) + + @ccall libngspice.ngSpice_Command(c_buffer::Ptr{Cchar})::Cint end - if ret != 0 - error("Failed to initialize julia extensions") - end - return true -end - -function ngSpice_Init_Sync(vsrcdat, isrcdat, syncdat, ident, userData) - ccall((:ngSpice_Init_Sync, libngspice), Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cint}, Ptr{Cvoid}), vsrcdat, isrcdat, syncdat, ident, userData) end -function ngSpice_Command(command) - ccall((:ngSpice_Command, libngspice), Cint, (Cstring,), command) -end - -function ngGet_Vec_Info(vecname) - ccall((:ngGet_Vec_Info, libngspice), pvector_info, (Cstring,), vecname) -end - -function ngSpice_Circ(circarray) - ccall((:ngSpice_Circ, libngspice), Cint, (Ptr{Ptr{UInt8}},), circarray) -end +#----------------------------------------------------------------- +# These seem safe function ngSpice_CurPlot() ccall((:ngSpice_CurPlot, libngspice), Ptr{UInt8}, ()) end @@ -40,14 +35,22 @@ function ngSpice_AllPlots() ccall((:ngSpice_AllPlots, libngspice), Ptr{Ptr{UInt8}}, ()) end + +#----------------------------------------------------------------- +# These functions are potentially unsafe due to passing String +# to Csting on foreign thread +function ngGet_Vec_Info(vecname) + ccall((:ngGet_Vec_Info, libngspice), pvector_info, (Cstring,), vecname) +end + + function ngSpice_AllVecs(plotname) ccall((:ngSpice_AllVecs, libngspice), Ptr{Ptr{UInt8}}, (Cstring,), plotname) end -function ngSpice_running() - ccall((:ngSpice_running, libngspice), Cint, ()) -end -function ngSpice_SetBkpt(bkpt) - ccall((:ngSpice_SetBkpt, libngspice), Cint, (Cdouble,), bkpt) +#----------------------------------------------------------------- +# Is this safe? +function ngSpice_Circ(circarray) + ccall((:ngSpice_Circ, libngspice), Cint, (Ptr{Ptr{UInt8}},), circarray) end diff --git a/src/interface/ngspice_common.jl b/src/interface/ngspice_common.jl index 61e2fc1..c7cae6c 100644 --- a/src/interface/ngspice_common.jl +++ b/src/interface/ngspice_common.jl @@ -1,4 +1,3 @@ -# Skipping MacroDefinition: IMPEXP __declspec ( dllimport ) const ngcomplex_t = Complex{Cdouble} @@ -11,6 +10,8 @@ const VF_MINGIVEN = (1 << 5) const VF_MAXGIVEN = (1 << 6) const VF_PERMANENT = (1 << 7) + + """ Vector info obtained from any vector in ngspice.dll. Allows direct access to the ngspice internal vector structure, @@ -53,9 +54,9 @@ struct vecinfo pdvecscale::Ptr{Cvoid} # a void pointer to struct dvec *ds, the scale vector end - const pvecinfo = Ptr{vecinfo} + struct vecinfoall name::Cstring title::Cstring @@ -64,85 +65,5 @@ struct vecinfoall veccount::Cint vecs::pvecinfo end +const pvecinfoall = Ptr{vecinfoall} -#const pvecinfoall = Ptr{vecinfoall} - -ngerrorf, bgrunningf, vecgetnum = 0, 0, 0 -function std_print(text) - occursin("stdout", text) && - (println(text[8:end]); return) - (occursin("stderr", text) && !occursin("viewport", text)) && - (println(text[8:end]); return) - occursin("viewport", text) && - (println("\nPlotting is skipped.\n") - ; return) - println(text) -end - -function sendchar(_text::Ptr{Cchar}, id::Cint, userdata)::Cint - _text != C_NULL || throw("Not a valid text") - text = unsafe_string(_text) - std_print(text) - occursin(r"stderr Error:"i, text) && (ngerrorf = 1) - return 0 -end - -gen_psendchar() = @cfunction(sendchar, Cint, (Ptr{Cchar}, Cint, Ptr{Cvoid})) - -function sendstat(_text::Ptr{Cchar}, id::Cint, userdata)::Cint - _text != C_NULL || throw("Not a valid text") - text = unsafe_string(_text) - std_print(text) - return 0 -end - -gen_psendstat() = @cfunction(sendstat, Cint, (Ptr{Cchar}, Cint, Ptr{Cvoid})) - -function bgthreadrunning(run::Cint, id::Cint, userdata::Ptr{Cvoid})::Cint - bgrunning = run - run ? println("BG thread is not running") : - println("BG thread is running") - return 0 -end - -gen_pbgthread() = @cfunction(bgthreadrunning, Cint, (Cint, Cint, Ptr{Cvoid})) - -function controlledexit(exitstatus::Cint, immediate::Cint, - quitexit::Cint, id::Cint, userdata::Ptr{Cvoid})::Cint - quitexit == 1 && println("Returned from quit with exit status") - immediate == 1 ? (println("Unloading NgSpice"); ngSpice_Command("quit")) : - (println("Prepare an unload"); will_unload = 1) - return exitstatus -end - -gen_pcontrolledexit() = @cfunction(controlledexit, Cint, (Cint, Cint, Cint, Cint, Ptr{Cvoid})) - -function senddata(vecdata::Ptr{vecinfoall}, - id::Cint, userdata::Ptr{Cvoid})::Cint - return 0 -end - -gen_psenddata() = @cfunction(senddata, Cint, (Ptr{vecinfoall}, Cint, Ptr{Cvoid})) - -function sendinitdata(initdata::Ptr{vecinfoall}, id::Cint, userdata::Ptr{Cvoid}) - #= - This bit is problematic because it always returns C_NULLs even before - `data.veccount` number of `vecinfo`s are passed. - Even when that is handled it's o/p is always `nothing` - and array of `nothing`s - - initdata == C_NULL && throw("No initialized data") - data = unsafe_load(initdata) - vec = unsafe_wrap(Array, data.vecs, (data.veccount, )) - for v in vec - v.name == C_NULL && return zero(Int32) - vname = unsafe_string(v.name) - vpdvec = unsafe_wrap(Array, v.pdvec, 10) - vpdscale = unsafe_wrap(Array, v.pdvecscale, 10) - println(vname, vpdscale, vpdvec) - end - =# - return zero(Int32) -end - -gen_psendinitdata() = @cfunction(sendinitdata, Cint, (Ptr{vecinfoall}, Cint, Ptr{Cvoid})) diff --git a/test/ngspice_api.jl b/test/ngspice_api.jl deleted file mode 100644 index eb1bc1f..0000000 --- a/test/ngspice_api.jl +++ /dev/null @@ -1,15 +0,0 @@ -using NgSpice -using Test - -rootdir = joinpath(@__DIR__, "..") -netpath = joinpath(rootdir, "inputs", "mosfet.cir") - -@testset "interface" begin - p = convert(Ptr{Nothing}, 0) - @test ngSpice_Init(p, p, p, p, p, p, p) == 0 - @test ngSpice_Command("source $netpath") == 0 - @test ngSpice_CurPlot() != C_NULL - @test ngSpice_Command("run") == 0 - @test typeof(ngSpice_AllPlots()) == Ptr{Ptr{UInt8}} - @test ngSpice_running() == 0 -end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl deleted file mode 100644 index 56b8194..0000000 --- a/test/runtests.jl +++ /dev/null @@ -1,15 +0,0 @@ -using NgSpice -using Test -using SafeTestsets - -@safetestset "interface functions" begin - include("ngspice_api.jl") -end - -@testset "friend functions" begin - include("wrapper_api.jl") -end - -@safetestset "wrapper safetests" begin - include("wrapper_safetests.jl") -end diff --git a/test/wrapper_api.jl b/test/wrapper_api.jl deleted file mode 100644 index 0aedb6c..0000000 --- a/test/wrapper_api.jl +++ /dev/null @@ -1,34 +0,0 @@ -using NgSpice -using Test - -rootdir = joinpath(@__DIR__, "..") -netpath = joinpath(rootdir, "inputs", "mosfet.cir") - -@testset "initialize" begin - init() - @test source(netpath) == 0 - @test NgSpice.run() == 0 -end - -@testset "getvectors" begin - # as this simulation runs after the one in `ngspice_api.jl`, - # `tran2` is the current plot - @test curplot() == "tran2" - @test listallplots() == ["tran2", "tran1", "const"] - @test collect(keys(listallvecs())) == collect(sort(listallplots())) - @test listallvecs()[curplot()] == listcurvecs() - @test getvec.(listcurvecs()) == get_vector_info.(listcurvecs()) - #listcurvecs() - for node in split("V(1) emit coll base vcc") - @test getvec(node)[2] == "voltage" - end - for branch in split("vcc#branch vin#branch") - @test getvec(branch)[2] == "current" - end - @test getvec("time")[2] == "time" -end - -@testset "passing circuit as an array" begin - netlist = readlines(netpath) - @test load_netlist(netlist) == 0 -end diff --git a/test/wrapper_safetests.jl b/test/wrapper_safetests.jl deleted file mode 100644 index 8cedacd..0000000 --- a/test/wrapper_safetests.jl +++ /dev/null @@ -1,16 +0,0 @@ -using NgSpice -using Test - -NgSpice.init() - -nl = """ -* Diode test circuit -V1 inp 0 DC=1 AC=1 SIN(1 1 1000) -* Connected in forward bias: -D1 inp 0 dmod -.model dmod d cjo=0 m=1 rs=5000 area=1 pj=0 cjp=0 mj=0 mjsw=0 pb=1 php=1 n=2 bv=0 ibv=0 ik=0 ikr=0 is=1.0e-9 tt=0 -.save all @d1[id] @d1[cd] @d1[charge] @d1[vd] -.END -""" - -@test NgSpice.load_netlist(nl) == 0 diff --git a/tutorials/demo.jl b/tutorials/demo.jl deleted file mode 100644 index 3e7d4d1..0000000 --- a/tutorials/demo.jl +++ /dev/null @@ -1,12 +0,0 @@ -# Check out mosfet.jl for more Julia-like usage - -import NgSpice -n = NgSpice - -netpath = joinpath(@__DIR__, "..", "inputs", "ac_ascii.cir") |> normpath - -n.init() -n.cmd("source $netpath") -n.cmd("run") -n.cmd("display") -#n.cmd("") diff --git a/tutorials/mosfet.jl b/tutorials/mosfet.jl deleted file mode 100644 index d4ec770..0000000 --- a/tutorials/mosfet.jl +++ /dev/null @@ -1,21 +0,0 @@ -using NgSpice -# using Plots -n = NgSpice - -netpath = joinpath(@__DIR__, "..", "inputs", "mosfet.cir") |> normpath - -n.init() -n.source(netpath) -n.run() -n.display() # Returns a dataframe of current vectors -n.listallvecs() # Lists vectors of both active and inactive plots -n.curplot() # Current active plot -n.listcurvecs() # Vectors in current active plot - -# For plotting the signals, pass `n.graph` an object of `NgSpiceGraphs` type. -# And then, specify the get-vector method. -# Pass "time" or "frequency" -# List of signals to plot -# Finally, the title of the plot -# plot(n.graph, n.getimagvec, "time", ["emit", "vcc"], "Simple MOSFET") -# plot(n.graph, n.getrealvec, "time", ["emit", "vcc"], "Simple MOSFET") From aa23c880028d11de13382d505f0b3c9674eb34f4 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Fri, 5 Jun 2026 11:30:20 -0600 Subject: [PATCH 04/12] Removed older github actions and netlist examples. --- .github/workflows/CompatHelper.yml | 16 ------------ .github/workflows/TagBot.yml | 16 ------------ .github/workflows/ci.yml | 40 ------------------------------ inputs/RC2.net | 15 ----------- inputs/ac_ascii.cir | 8 ------ inputs/mosfet.cir | 23 ----------------- inputs/vdiv.cir | 5 ---- 7 files changed, 123 deletions(-) delete mode 100644 .github/workflows/CompatHelper.yml delete mode 100644 .github/workflows/TagBot.yml delete mode 100644 .github/workflows/ci.yml delete mode 100644 inputs/RC2.net delete mode 100644 inputs/ac_ascii.cir delete mode 100644 inputs/mosfet.cir delete mode 100644 inputs/vdiv.cir diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml deleted file mode 100644 index cba9134..0000000 --- a/.github/workflows/CompatHelper.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: CompatHelper -on: - schedule: - - cron: 0 0 * * * - workflow_dispatch: -jobs: - CompatHelper: - runs-on: ubuntu-latest - steps: - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml deleted file mode 100644 index 634f90d..0000000 --- a/.github/workflows/TagBot.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: TagBot -on: - issue_comment: - types: - - created - workflow_dispatch: -jobs: - TagBot: - if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' - runs-on: ubuntu-latest - steps: - - uses: JuliaRegistries/TagBot@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - ssh: ${{ secrets.DOCUMENTER_KEY }} - \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index a034182..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: CI -on: - push: - branches: - - master - tags: '*' - pull_request: -jobs: - test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - version: - - '1' - os: - - ubuntu-latest - - macOS-latest - - windows-latest - arch: - - x64 - steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-runtest@latest \ No newline at end of file diff --git a/inputs/RC2.net b/inputs/RC2.net deleted file mode 100644 index e08dc85..0000000 --- a/inputs/RC2.net +++ /dev/null @@ -1,15 +0,0 @@ -* Spice netlister for gnetlist -R5 n4 n5 1k -V1 n0 0 dc 1 ac 2 pulse 0 1 10n 10n 100n 1u 2u -R4 n3 n4 1k -R3 n2 n3 5k -C5 n5 0 1n -R2 n1 n2 1K -C4 n4 0 1n -R1 n0 n1 1k -C3 n3 0 1n -C2 n2 0 1n -C1 n1 0 1n -I1 n5 0 DC 0.01mA -R6 0 n5 10k -.END \ No newline at end of file diff --git a/inputs/ac_ascii.cir b/inputs/ac_ascii.cir deleted file mode 100644 index f7f14a5..0000000 --- a/inputs/ac_ascii.cir +++ /dev/null @@ -1,8 +0,0 @@ -* simulation of RC2 -.control -set filetype=ascii -ac dec 100 10k 1000k - -write ac_ascii.raw -.endc -.include RC2.net \ No newline at end of file diff --git a/inputs/mosfet.cir b/inputs/mosfet.cir deleted file mode 100644 index 1d25d40..0000000 --- a/inputs/mosfet.cir +++ /dev/null @@ -1,23 +0,0 @@ -A Berkeley SPICE3 compatible circuit -* -* This circuit contains only Berkeley SPICE3 components. -* -* The circuit is an AC coupled transistor amplifier with -* a sinewave input at node "1", a gain of approximately -3.9, -* and output on node "coll". -* -.tran 1e-5 2e-3 -* -vcc vcc 0 12.0 -vin 1 0 0.0 ac 1.0 sin(0 1 1k) -ccouple 1 base 10uF -rbias1 vcc base 100k -rbias2 base 0 24k -q1 coll base emit generic -rcollector vcc coll 3.9k -remitter emit 0 1k -* -.probe v(emit) v(vcc) v(1) v(base) v(coll) vcc#branch vin#branch -.model generic npn -* -.end \ No newline at end of file diff --git a/inputs/vdiv.cir b/inputs/vdiv.cir deleted file mode 100644 index db5ae1f..0000000 --- a/inputs/vdiv.cir +++ /dev/null @@ -1,5 +0,0 @@ -voltage divider netlist -V1 in 0 1 -R1 in out 1k -R2 out 0 2k -.end \ No newline at end of file From 4992efc2e60f51cf92437f54fd04c6c75a26b518 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Fri, 5 Jun 2026 11:33:47 -0600 Subject: [PATCH 05/12] Removed redundant NgSpice.jl file --- src/NgSpice.jl | 51 -------------------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 src/NgSpice.jl diff --git a/src/NgSpice.jl b/src/NgSpice.jl deleted file mode 100644 index c243739..0000000 --- a/src/NgSpice.jl +++ /dev/null @@ -1,51 +0,0 @@ -__precompile__() -module NgSpice - -using ngspice_jll -export ngspice_jll - -using Requires - -include("interface/ctypes.jl") -export Ctm, Ctime_t, Cclock_t - -include("interface/ngspice_common.jl") -include("interface/ngspice_api.jl") - -export bgthreadrunning, controlledexit, #getISRCdata, detsyncdata, getVSRCdata, - sendchar, senddata, sendstat, sendinitdata - -export gen_pbgthread, gen_pcontrolledexit, gen_psendchar, gen_psenddata, - gen_psendinitdata, gen_psendstat - -foreach(names(@__MODULE__, all=true)) do s - if startswith(string(s), "ng") - @eval export $s - end -end #should this be replaced with aliases - -include("extensions.jl") - -include("API/get_vector.jl") -include("API/sim_utils.jl") -include("API/running.jl") - -function __init__() -# @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" @eval include("API/repl.jl") -# @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" @eval include("API/graphs.jl") - - # Ensure that NgSpice is initialized, as otherwise it's easy to get segfaults - # because NgSpice expects a printf callback to be provided (as we do in the - # init() method we're calling here) - init() -end - -export get_vector_info, - curplot, listallplots, listallvecs, listcurvecs, getimagvec, - getmagnitudevec, getphasevec, getvec, getrealvec, - NgSpiceGraphs, graph, - load_netlist, source, source_sp, - bghalt, bgrun, cmd, init, - interactive, alter - #isrunning, -end From 3f0a018102584047acab8eb35e6874e584e1f56c Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Fri, 5 Jun 2026 13:11:21 -0600 Subject: [PATCH 06/12] Disabling garbage collection while processing the async condition. Still seeing rare random segfaults. --- README.md | 16 +++++++++++ src/API/sim_utils.jl | 8 +++--- src/NgHerb.jl | 55 +++++++++++++++++++++++++++++++++++++- src/interface/callbacks.jl | 2 +- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3dcdc3a..fb5f7fe 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,19 @@ julia> NgHerb.getrealvec("2") ``` +## String Macros + +Some special strings are defined to conveniently interface with `ngspice`: + +* `ng"..."` -- shorthand for NgHerb.cmd() +* `vec"..."` -- returns the indicated vector (with name and type) + +These string types all return `Vector{Float64}`: + +* `real"..."` -- real part of the indicated `ngspice` vector +* `imag"..."` -- imaginary part of the indicated `ngspice` vector +* `magnitude"..."` -- complex magnitude of the indicated vector +* `phase"..."` -- complex phase (in degrees) +* `dB"..."` -- magnitude in dB20 +* `i"..."` -- real-valued branch current in the indicated voltage source + diff --git a/src/API/sim_utils.jl b/src/API/sim_utils.jl index 50d1607..a188188 100644 --- a/src/API/sim_utils.jl +++ b/src/API/sim_utils.jl @@ -1,11 +1,11 @@ - function load_netlist(netlist::AbstractArray{T}) where T <: AbstractString + function load_netlist(netlist::Vector{String}) append!(netlist, ["C_NULL"]) t = netlist |> ngSpice_Circ end # Opted to use the "circbyline" approach here to reduce # memory crossover events between Julia and NgSpice. -function load_netlist(netlist::AbstractString) +function load_netlist(netlist::String) for line in eachsplit(netlist,"\n") if contains(line,r"\w+") cmd(string("circbyline ",line)) @@ -15,7 +15,9 @@ function load_netlist(netlist::AbstractString) cmd("circbyline .end") end end +precompile(load_netlist,(String,)) +precompile(load_netlist,(Vector{String},)) -source(netpath) = cmd("source $netpath") +source(netpath::String) = cmd("source $netpath") display() = cmd("display") diff --git a/src/NgHerb.jl b/src/NgHerb.jl index 29d12db..0b76d0f 100644 --- a/src/NgHerb.jl +++ b/src/NgHerb.jl @@ -50,8 +50,11 @@ function __init__() @async begin try while isopen(async_cond[]) - wait(async_cond[]) + GC.gc() + wait(async_cond[]) + GC.enable(false) + # Dump contents from ring buffer buf_str = String(collect(string_buffer)) empty!(string_buffer) @@ -64,6 +67,8 @@ function __init__() # Print everything println(String(take!(seekstart(io)))) + + GC.enable(true) end catch err @error "Error in AsyncCondition processing loop" exception=(err, catch_backtrace()) @@ -72,4 +77,52 @@ function __init__() init() end + + +#================ Special Strings =====================# +# ng"" sends the quoted command to the simulator +macro ng_str(s) + NgHerb.cmd(s) +end + +# real"" retrieves the real-valued part of the indicated vector +macro real_str(s) + NgHerb.getrealvec(s) +end + +# imag"" retrieves the imaginary-valued part of the indicated vector +macro imag_str(s) + NgHerb.getimaginaryvec(s) +end + +# i"" retrieves the current in the indicated voltage source +macro i_str(s) + NgHerb.getrealvec(s*"#branch") +end + +# magnitude"" retrieves the complex magnitude of the indicated vector +macro magnitude_str(s) + NgHerb.getmagnitudevec(s) +end + +# dB"" retrieves a magnitude vector and converts to dB20 +macro dB_str(s) + 20.0 .* log10.(NgHerb.getmagnitudevec(s)) +end + +# phase"" retrieves a phase vector and converts to degrees +macro phase_str(s) + (180/π).*NgHerb.getphasevec(s) +end + +# vec"" returns a vector, possibly complex +macro vec_str(s) + NgHerb.getvec(s) +end + + + + +export @ng_str, @real_str, @imag_str, @i_str, @magnitude_str, @dB_str, @phase_str, @vec_str + end diff --git a/src/interface/callbacks.jl b/src/interface/callbacks.jl index 399872e..6bed9b2 100644 --- a/src/interface/callbacks.jl +++ b/src/interface/callbacks.jl @@ -8,7 +8,7 @@ function sendchar(_text::Ptr{Cchar}, id::Cint, userdata::Ptr{Cvoid})::Cint ccall(:uv_async_send, Cint, (Ptr{Cvoid},), async_cond[].handle) return zero(Int32) end - +precompile(sendchar,(Ptr{Cchar},Cint,Ptr{Cvoid})) From 4bfb443edfb7a852950992a3c4ceccc83cdac9c8 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Fri, 5 Jun 2026 22:51:08 -0600 Subject: [PATCH 07/12] This probably isn't the best solution (blocking garbage collection most of the time), but I haven't seen segfaults in a while. --- src/NgHerb.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NgHerb.jl b/src/NgHerb.jl index 0b76d0f..d614509 100644 --- a/src/NgHerb.jl +++ b/src/NgHerb.jl @@ -50,10 +50,11 @@ function __init__() @async begin try while isopen(async_cond[]) + GC.enable(true) GC.gc() + GC.enable(false) wait(async_cond[]) - GC.enable(false) # Dump contents from ring buffer buf_str = String(collect(string_buffer)) @@ -68,7 +69,7 @@ function __init__() # Print everything println(String(take!(seekstart(io)))) - GC.enable(true) + end catch err @error "Error in AsyncCondition processing loop" exception=(err, catch_backtrace()) From 3268cf16f037ff4c92097e13e6c8987c968447c9 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Thu, 11 Jun 2026 13:49:37 -0600 Subject: [PATCH 08/12] Trying a polling mechanism instead of the async condition. Introduced a brief pause after ngspice library commands to allow callbacks to return. --- src/API/running.jl | 11 +++++++++-- src/NgHerb.jl | 34 ++++++++++++++++++++++++++++++---- src/interface/callbacks.jl | 4 ++-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/API/running.jl b/src/API/running.jl index aed8159..c1277a7 100644 --- a/src/API/running.jl +++ b/src/API/running.jl @@ -1,8 +1,15 @@ -cmd(command) = GC.@preserve command ngSpice_Command(command) + + +function cmd(command) + GC.@preserve command ngSpice_Command(command) + sleep(0.1) + dumpbuffer() +end precompile(cmd,(String,)) + function init() - #pvoid = convert(Ptr{Nothing}, 0) + ngSpice_Init(gen_psendchar[], gen_psendstat[], gen_pcontrolledexit[], gen_psenddata[], diff --git a/src/NgHerb.jl b/src/NgHerb.jl index d614509..b57ac63 100644 --- a/src/NgHerb.jl +++ b/src/NgHerb.jl @@ -13,7 +13,7 @@ include("API/sim_utils.jl") include("API/running.jl") include("API/get_vector.jl") -const async_cond = Ref{Base.AsyncCondition}() +#const async_cond = Ref{Base.AsyncCondition}() string_buffer = CircularBuffer{UInt8}(10000) # Callback pointers @@ -32,7 +32,7 @@ data_pointer = Ptr{vecinfoall}(0) function __init__() - async_cond[] = Base.AsyncCondition() + #async_cond[] = Base.AsyncCondition() # We will just need these callbacks: gen_psendchar[] = @cfunction(sendchar, Cint, (Ptr{Cchar}, Cint, Ptr{Cvoid} )) @@ -46,7 +46,15 @@ function __init__() push!(cbvec,x) end - # Asynchronous wait loop for ngspice library messages + init() + + sleep(0.1) + dumpbuffer() +end + + + +function callback_listener() @async begin try while isopen(async_cond[]) @@ -75,10 +83,28 @@ function __init__() @error "Error in AsyncCondition processing loop" exception=(err, catch_backtrace()) end end - init() end +function dumpbuffer() + try + # Dump contents from ring buffer + buf_str = String(collect(string_buffer)) + empty!(string_buffer) + + # Remove "stdout " from start of lines + io=IOBuffer() + for l in eachsplit(buf_str,"\n") + println(io,replace(l,r"^stdout "=>"")) + end + + # Print everything + println(String(take!(seekstart(io)))) + catch err + @error "Error in AsyncCondition processing loop" exception=(err, catch_backtrace()) + end +end + #================ Special Strings =====================# # ng"" sends the quoted command to the simulator diff --git a/src/interface/callbacks.jl b/src/interface/callbacks.jl index 6bed9b2..9eb635b 100644 --- a/src/interface/callbacks.jl +++ b/src/interface/callbacks.jl @@ -4,8 +4,8 @@ function sendchar(_text::Ptr{Cchar}, id::Cint, userdata::Ptr{Cvoid})::Cint push!(string_buffer,unsafe_load(_text,c)) end push!(string_buffer,'\n') - - ccall(:uv_async_send, Cint, (Ptr{Cvoid},), async_cond[].handle) + + #ccall(:uv_async_send, Cint, (Ptr{Cvoid},), async_cond[].handle) return zero(Int32) end precompile(sendchar,(Ptr{Cchar},Cint,Ptr{Cvoid})) From 0c810aafdb8e953f171bdcadeeb8be6af82847e1 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Fri, 12 Jun 2026 13:25:14 -0600 Subject: [PATCH 09/12] Removed callback polling, rearranged GC disabling so that it should only happen when calling an ngspice library function. --- Project.toml | 1 + src/API/get_vector.jl | 2 ++ src/API/running.jl | 9 +++--- src/NgHerb.jl | 65 +++++++++++++------------------------- src/interface/callbacks.jl | 2 +- 5 files changed, 30 insertions(+), 49 deletions(-) diff --git a/Project.toml b/Project.toml index e474b95..99a61b6 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ authors = ["Chris Winstead", "Venkateshprasad Bhat"] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" ngspice_jll = "57e080ce-e004-5ece-a144-c95753423b2a" + [compat] DataStructures = "0.19.5" ngspice_jll = "41.0.0" diff --git a/src/API/get_vector.jl b/src/API/get_vector.jl index d026032..1087e1b 100644 --- a/src/API/get_vector.jl +++ b/src/API/get_vector.jl @@ -1,4 +1,5 @@ function get_vector_info(vecname, maxlen=Int(maxintfloat())) + GC.enable(false) factor = 1.0 if occursin("*", vecname) factor, vecname = split(vecname, "*") @@ -43,6 +44,7 @@ function get_vector_info(vecname, maxlen=Int(maxintfloat())) else error("Unknown vector type") end + GC.enable(true) end function curplot() diff --git a/src/API/running.jl b/src/API/running.jl index c1277a7..c01ee97 100644 --- a/src/API/running.jl +++ b/src/API/running.jl @@ -1,15 +1,14 @@ -function cmd(command) - GC.@preserve command ngSpice_Command(command) - sleep(0.1) - dumpbuffer() +function cmd(command::String) + GC.enable(false) + ngSpice_Command(command) end precompile(cmd,(String,)) function init() - + GC.enable(false) ngSpice_Init(gen_psendchar[], gen_psendstat[], gen_pcontrolledexit[], gen_psenddata[], diff --git a/src/NgHerb.jl b/src/NgHerb.jl index b57ac63..8811f6c 100644 --- a/src/NgHerb.jl +++ b/src/NgHerb.jl @@ -13,7 +13,7 @@ include("API/sim_utils.jl") include("API/running.jl") include("API/get_vector.jl") -#const async_cond = Ref{Base.AsyncCondition}() +const async_cond = Ref{Base.AsyncCondition}() string_buffer = CircularBuffer{UInt8}(10000) # Callback pointers @@ -32,7 +32,7 @@ data_pointer = Ptr{vecinfoall}(0) function __init__() - #async_cond[] = Base.AsyncCondition() + async_cond[] = Base.AsyncCondition() # We will just need these callbacks: gen_psendchar[] = @cfunction(sendchar, Cint, (Ptr{Cchar}, Cint, Ptr{Cvoid} )) @@ -46,10 +46,26 @@ function __init__() push!(cbvec,x) end + + callback_listener() init() +end + - sleep(0.1) - dumpbuffer() + +function dumpbuffer() + # Dump contents from ring buffer + buf_str = String(collect(string_buffer)) + empty!(string_buffer) + + # Remove "stdout " from start of lines + io=IOBuffer() + for l in eachsplit(buf_str,"\n") + println(io,replace(l,r"^stdout "=>"")) + end + + # Print everything + println(String(take!(seekstart(io)))) end @@ -58,26 +74,9 @@ function callback_listener() @async begin try while isopen(async_cond[]) - GC.enable(true) - GC.gc() - GC.enable(false) - wait(async_cond[]) - - # Dump contents from ring buffer - buf_str = String(collect(string_buffer)) - empty!(string_buffer) - - # Remove "stdout " from start of lines - io=IOBuffer() - for l in eachsplit(buf_str,"\n") - println(io,replace(l,r"^stdout "=>"")) - end - - # Print everything - println(String(take!(seekstart(io)))) - - + dumpbuffer() + GC.enable(true) end catch err @error "Error in AsyncCondition processing loop" exception=(err, catch_backtrace()) @@ -86,26 +85,6 @@ function callback_listener() end -function dumpbuffer() - try - # Dump contents from ring buffer - buf_str = String(collect(string_buffer)) - empty!(string_buffer) - - # Remove "stdout " from start of lines - io=IOBuffer() - for l in eachsplit(buf_str,"\n") - println(io,replace(l,r"^stdout "=>"")) - end - - # Print everything - println(String(take!(seekstart(io)))) - catch err - @error "Error in AsyncCondition processing loop" exception=(err, catch_backtrace()) - end -end - - #================ Special Strings =====================# # ng"" sends the quoted command to the simulator macro ng_str(s) diff --git a/src/interface/callbacks.jl b/src/interface/callbacks.jl index 9eb635b..3671fdb 100644 --- a/src/interface/callbacks.jl +++ b/src/interface/callbacks.jl @@ -5,7 +5,7 @@ function sendchar(_text::Ptr{Cchar}, id::Cint, userdata::Ptr{Cvoid})::Cint end push!(string_buffer,'\n') - #ccall(:uv_async_send, Cint, (Ptr{Cvoid},), async_cond[].handle) + ccall(:uv_async_send, Cint, (Ptr{Cvoid},), async_cond[].handle) return zero(Int32) end precompile(sendchar,(Ptr{Cchar},Cint,Ptr{Cvoid})) From af34f2fffce28b30b0179c7cdebaa188dbe86774 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Tue, 25 Aug 2026 10:42:42 -0600 Subject: [PATCH 10/12] Suppressed all of the async I/O, now using a function called after commands complete. Callbacks just push text into the circular buffer. Also added a function to suppress output. --- src/API/get_vector.jl | 4 ++-- src/API/running.jl | 4 ++++ src/NgHerb.jl | 19 ++++++++++++------- src/interface/callbacks.jl | 2 +- src/interface/ngspice_api.jl | 2 +- src/interface/ngspice_common.jl | 26 +++++++++++++------------- 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/API/get_vector.jl b/src/API/get_vector.jl index 1087e1b..ad63169 100644 --- a/src/API/get_vector.jl +++ b/src/API/get_vector.jl @@ -5,9 +5,9 @@ function get_vector_info(vecname, maxlen=Int(maxintfloat())) factor, vecname = split(vecname, "*") factor = parse(Float64, factor) end - vec = ngGet_Vec_Info(vecname) + vec = ngGet_Vec_Info(vecname) # returns pVectorInfo vec != C_NULL || throw("Vector $(vecname) not found") - vecinfo = unsafe_load(vec) + vecinfo = unsafe_load(vec) # vecinfo should be a VectorInfo vname = unsafe_string(vecinfo.name) len = min(maxlen, vecinfo.length) typelist = Dict(0 => "notype", diff --git a/src/API/running.jl b/src/API/running.jl index c01ee97..935f556 100644 --- a/src/API/running.jl +++ b/src/API/running.jl @@ -1,8 +1,12 @@ function cmd(command::String) + global quiet GC.enable(false) ngSpice_Command(command) + if !quiet + dumpbuffer() + end end precompile(cmd,(String,)) diff --git a/src/NgHerb.jl b/src/NgHerb.jl index 8811f6c..5bc2246 100644 --- a/src/NgHerb.jl +++ b/src/NgHerb.jl @@ -13,7 +13,7 @@ include("API/sim_utils.jl") include("API/running.jl") include("API/get_vector.jl") -const async_cond = Ref{Base.AsyncCondition}() +#const async_cond = Ref{Base.AsyncCondition}() string_buffer = CircularBuffer{UInt8}(10000) # Callback pointers @@ -27,12 +27,13 @@ const gen_psendinitdata = Ref{Ptr{Cvoid}}(C_NULL) pcbvec = Vector{Ref}() cbvec = Vector{Function}() +quiet = false -data_pointer = Ptr{vecinfoall}(0) +data_pointer = Ptr{VecInfoAll}(0) function __init__() - async_cond[] = Base.AsyncCondition() + # async_cond[] = Base.AsyncCondition() # We will just need these callbacks: gen_psendchar[] = @cfunction(sendchar, Cint, (Ptr{Cchar}, Cint, Ptr{Cvoid} )) @@ -47,10 +48,13 @@ function __init__() end - callback_listener() +# callback_listener() init() end +function be_quiet() + global quiet = true +end function dumpbuffer() @@ -69,13 +73,13 @@ function dumpbuffer() end - +#= function callback_listener() @async begin try while isopen(async_cond[]) wait(async_cond[]) - dumpbuffer() + #dumpbuffer() GC.enable(true) end catch err @@ -83,7 +87,7 @@ function callback_listener() end end end - +=# #================ Special Strings =====================# # ng"" sends the quoted command to the simulator @@ -130,5 +134,6 @@ end export @ng_str, @real_str, @imag_str, @i_str, @magnitude_str, @dB_str, @phase_str, @vec_str +export dump_buffer,be_quiet end diff --git a/src/interface/callbacks.jl b/src/interface/callbacks.jl index 3671fdb..9eb635b 100644 --- a/src/interface/callbacks.jl +++ b/src/interface/callbacks.jl @@ -5,7 +5,7 @@ function sendchar(_text::Ptr{Cchar}, id::Cint, userdata::Ptr{Cvoid})::Cint end push!(string_buffer,'\n') - ccall(:uv_async_send, Cint, (Ptr{Cvoid},), async_cond[].handle) + #ccall(:uv_async_send, Cint, (Ptr{Cvoid},), async_cond[].handle) return zero(Int32) end precompile(sendchar,(Ptr{Cchar},Cint,Ptr{Cvoid})) diff --git a/src/interface/ngspice_api.jl b/src/interface/ngspice_api.jl index 562f0c8..c423295 100644 --- a/src/interface/ngspice_api.jl +++ b/src/interface/ngspice_api.jl @@ -40,7 +40,7 @@ end # These functions are potentially unsafe due to passing String # to Csting on foreign thread function ngGet_Vec_Info(vecname) - ccall((:ngGet_Vec_Info, libngspice), pvector_info, (Cstring,), vecname) + ccall((:ngGet_Vec_Info, libngspice), pVectorInfo, (Cstring,), vecname) end diff --git a/src/interface/ngspice_common.jl b/src/interface/ngspice_common.jl index c7cae6c..b845418 100644 --- a/src/interface/ngspice_common.jl +++ b/src/interface/ngspice_common.jl @@ -17,7 +17,7 @@ const VF_PERMANENT = (1 << 7) Allows direct access to the ngspice internal vector structure, as defined in include/ngspice/devc.h . """ -struct vector_info +struct VectorInfo name::Cstring # Same as so_vname type::Cint # Same as so_vtype flags::Int16 # Flags (a combination of VF_*) @@ -26,9 +26,9 @@ struct vector_info length::Cint # Length of the vector end -const pvector_info = Ptr{vector_info} +const pVectorInfo = Ptr{VectorInfo} -struct vecvalues +struct VecValues name::Cstring # name of a specific vector creal::Cdouble # actual data value cimag::Cdouble # actual data value @@ -36,17 +36,17 @@ struct vecvalues is_complex::Cint # if the data are complex numbers end -const pvecvalues = Ptr{vecvalues} +const pVecValues = Ptr{VecValues} -struct vecvaluesall +struct VecValuesAll veccount::Cint # number of vectors in plot vecindex::Cint # index of actual set of vectors. i.e. the number of accepted data point - vecsa::pvecvalues # values of actual set of vectors, indexed from 0 to veccount - 1 - #just pvecvalues? + vecsa::pVecValues # values of actual set of vectors, indexed from 0 to veccount - 1 + #just pVecValues? end -const pvecvaluesall = Ptr{vecvaluesall} -struct vecinfo +const pVecValuesAll = Ptr{VecValuesAll} +struct VecInfo number::Cint # number of vector, as postion in the linked list of vectors, s name::Cstring # name of the actual vector is_real::Cint # 1 if the actual vector has real data @@ -54,16 +54,16 @@ struct vecinfo pdvecscale::Ptr{Cvoid} # a void pointer to struct dvec *ds, the scale vector end -const pvecinfo = Ptr{vecinfo} +const pVecInfo = Ptr{VecInfo} -struct vecinfoall +struct VecInfoAll name::Cstring title::Cstring date::Cstring type::Cstring veccount::Cint - vecs::pvecinfo + vecs::pVecInfo end -const pvecinfoall = Ptr{vecinfoall} +const pVecInfoAll = Ptr{VecInfoAll} From 126fc8c6270693d3fae6cf0b9b571a653403c290 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Tue, 25 Aug 2026 10:53:37 -0600 Subject: [PATCH 11/12] Update README.md --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fb5f7fe..4358959 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ -# NgHerb -- A Revision of NgSpice +# NgHerb.jl -- A Revision of NgSpice.jl -This repository provides a Julia wrapper for NgSpice library. It's based on the -[NgSpice](https://github.com/JuliaComputing/NgSpice.jl) Julia module by Venkateshprasad Bhat. -NgHerb implements these changes: +This repository provides a Julia wrapper for `ngspice` shared C library. It's based on the +[NgSpice.jl](https://github.com/JuliaComputing/NgSpice.jl) Julia module by Venkateshprasad Bhat. +NgHerb.jl implements these changes: -* Thread safety for NgSpice callbacks, to resolve segfaults in Julia 12.6+ +* Thread safety for `ngspice` callbacks, to resolve segfaults in Julia 1.12.6+ * Removed unused callbacks -* Removed REPL and Plot features for easier maintenance +* Removed some REPL and Plot features for easier maintenance -NgHerb has some additional streamlining compared to NgSpice. +NgHerb.jl has some additional streamlining compared to NgSpice.jl -The main purpose is to provide a minimal `ngspice` library wrapper that works stably with more recent -Julia versions. As of Julia 12.6, the original NgSpice module is broken unless -Julia is constrained to one thread, i.e. `julia -t 1`. NgHerb tries to fix this by -introducing a CircularBuffer to manage asyncrhonous output from the `ngspice`. +The main purpose of this fork is to provide a minimal `ngspice` library wrapper that works stably with more recent +Julia versions. As of Julia 1.12.6, the upstream NgSpice.jl module is broken unless +Julia is constrained to one thread, i.e. `julia -t 1`. NgHerb.jl tries to fix this by +introducing a CircularBuffer to manage asynchronous output from the `ngspice`. I made a fork rather than +mess with NgSpice.jl directly, mainly because I'm not super confident about my solution. ## Usage: From 4626308ecf1a5fc289be523fcc7820e18d11a021 Mon Sep 17 00:00:00 2001 From: Chris Winstead Date: Wed, 2 Sep 2026 11:33:35 -0600 Subject: [PATCH 12/12] Fixed version string in Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 99a61b6..cc3b534 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "NgHerb" uuid = "16d751f2-2168-46b0-9d00-9e1470832ba3" -version = "0.2.1" +version = "0.2.0" authors = ["Chris Winstead", "Venkateshprasad Bhat"] [deps]