Skip to content

Commit 67f9cc7

Browse files
authored
neuronapi: add nrn_symbol_pop; make nrn_object_pop nil-safe (#3836)
* neuronapi: add nrn_symbol_pop and nrn_object_pop_safe Unwinding an interpreter frame from a binding -- the HOC-to-Python component read/write-back path (pyobj.attr) -- needs two stack pops the public API does not have, so it reaches for the mangled internals instead. nrn_symbol_pop wraps hoc_spop: the interpreter leaves the attribute's Symbol on the stack during object-component access, and unwinding that frame has to pop it. nrn_object_pop_safe is nrn_object_pop but nil-tolerant. nrn_object_pop unconditionally does (*obptr)->refcount++ to hand back a reference, which dereferences NULL when the stack entry is a nil objref (an unset objref RHS in a write-back, say). The safe variant guards that one ref; the stack-slot handling is otherwise identical, and a non-NULL result is still reffed and should be unref'd. test/api/stack_pops.cpp pushes symbols and pops them LIFO, round-trips a real object through nrn_object_pop_safe, and confirms a nil objref pops to NULL rather than crashing, with the stack left balanced. * neuronapi: make nrn_object_pop nil-safe; drop nrn_object_pop_safe Address review on #3836. Rather than adding a parallel nrn_object_pop_safe, fold the nil guard into nrn_object_pop itself: it now returns NULL for a nil object reference (an unset objref) instead of dereferencing NULL to bump the refcount. The crash-on-nil behavior was not useful to any caller, so this is strictly more permissive -- identical for a real object, NULL instead of a segfault for nil. All 12 api:: tests pass. nrn_symbol_pop is unchanged. Docs and the stack_pops test updated to the single function. * stack_pops test: sentinel-below stack-hygiene check Address review on #3836 (same fix as #3828's setpointer test): the trailing 'push 5.0, pop, expect 5.0' proved nothing -- a stack returns the most recently pushed item, so a sentinel pushed after the pops only probes the current top. Push the sentinel FIRST, before any symbol/object push, and recover it at the end; that proves the pops neither over-popped into what lay beneath nor left anything behind.
1 parent e99f020 commit 67f9cc7

5 files changed

Lines changed: 117 additions & 6 deletions

File tree

docs/capi.rst

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,18 @@ Functions, objects, and the stack
871871
872872
:param sym: Pointer to the symbol to push.
873873
874+
.. c:function:: Symbol* nrn_symbol_pop(void)
875+
876+
Pop a Symbol from the top of the stack.
877+
878+
The interpreter puts a Symbol on the stack when accessing an object
879+
component (reading or assigning ``pyobj.attr``); a binding that unwinds
880+
such a stack frame uses this to pop the attribute's Symbol. Use
881+
:c:func:`nrn_stack_type` to confirm the top is a ``STACK_IS_SYM`` entry
882+
before popping.
883+
884+
:returns: The Symbol from the top of the stack.
885+
874886
.. c:function:: int nrn_symbol_type(const Symbol* sym)
875887
876888
Get the type of a symbol (e.g., function, variable, mechanism).
@@ -1126,13 +1138,20 @@ Functions, objects, and the stack
11261138
11271139
Pop an object from the stack.
11281140
1129-
:returns: Pointer to object from the top of the stack.
1141+
Returns ``NULL`` for a nil object reference (an unset ``objref``) rather than
1142+
crashing, so it is safe to use when unwinding a stack that may carry a nil
1143+
object -- for example a HOC-to-Python write-back whose right-hand side is an
1144+
unset ``objref``.
1145+
1146+
:returns: Pointer to the object from the top of the stack, or ``NULL`` if it
1147+
is a nil object reference.
11301148
11311149
**Usage Pattern:**
11321150
11331151
Used to retrieve function/method return values. Use :c:func:`nrn_stack_type` to check the type
11341152
before popping, or use the type of the function/method to know the expected return type in
1135-
advance.
1153+
advance. A non-``NULL`` result is reference-counted and should be released with
1154+
:c:func:`nrn_object_unref` when no longer needed.
11361155
11371156
.. c:function:: nrn_stack_types_t nrn_stack_type(void)
11381157

src/nrniv/neuronapi.cpp

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,14 @@ void nrn_symbol_push(Symbol* sym) {
463463
hoc_pushpx(sym->u.pval);
464464
}
465465

466+
Symbol* nrn_symbol_pop(void) {
467+
// Pop a Symbol (a STACK_IS_SYM entry) off the interpreter stack. Interpreter
468+
// frames for object-component access (e.g. reading or assigning pyobj.attr)
469+
// carry the attribute's Symbol on the stack; a binding that unwinds such a
470+
// frame needs to pop it. Public counterpart to the internal hoc_spop.
471+
return hoc_spop();
472+
}
473+
466474
void nrn_double_push(double val) {
467475
hoc_pushx(val);
468476
}
@@ -511,12 +519,19 @@ void nrn_object_ptr_push(Object** obj_ref) {
511519
}
512520

513521
Object* nrn_object_pop(void) {
514-
// NOTE: the returned object should be unref'd when no longer needed
522+
// Returns NULL for a nil object reference (an unset objref) rather than
523+
// crashing: the ref-count bump that hands back a reference would otherwise
524+
// dereference NULL. This matters when unwinding a stack that may carry a nil
525+
// object -- e.g. the HOC-to-Python write-back path, where an objref RHS can
526+
// be nil. A non-NULL result is reference-counted and should be unref'd
527+
// (nrn_object_unref) when no longer needed.
515528
Object** obptr = hoc_objpop();
516-
Object* new_ob_ptr = *obptr;
517-
new_ob_ptr->refcount++;
529+
Object* ob = *obptr;
530+
if (ob) {
531+
ob->refcount++;
532+
}
518533
hoc_tobj_unref(obptr);
519-
return new_ob_ptr;
534+
return ob;
520535
}
521536

522537
nrn_stack_types_t nrn_stack_type(void) {

src/nrniv/neuronapi.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ int nrn_pp_setpointer_pop(Object* pp, const char* name, char* error_msg, size_t
8888
****************************************/
8989
Symbol* nrn_symbol(const char* name);
9090
void nrn_symbol_push(Symbol* sym);
91+
Symbol* nrn_symbol_pop(void);
9192
int nrn_symbol_type(const Symbol* sym);
9293
int nrn_symbol_subtype(const Symbol* sym);
9394
double* nrn_symbol_dataptr(const Symbol* sym);

test/api/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ foreach(
1010
section_tree.cpp
1111
segment_diam.cpp
1212
sections.cpp
13+
stack_pops.cpp
1314
symbol_object_string.cpp
1415
vclamp.cpp)
1516
string(REPLACE "." "_" api_test_name "${api_test_file}")

test/api/stack_pops.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// NOTE: this assumes neuronapi.h is on your CPLUS_INCLUDE_PATH
2+
// Exercises nrn_symbol_pop and nrn_object_pop's nil handling, the stack-pop
3+
// primitives used to unwind an interpreter frame from a binding (the
4+
// HOC-to-Python component read/write-back path). nrn_symbol_pop pops a Symbol
5+
// the interpreter left on the stack; nrn_object_pop pops an object but returns
6+
// NULL for a nil objref instead of dereferencing it to take a reference.
7+
#include <array>
8+
#include <iostream>
9+
#include "neuronapi.h"
10+
11+
using std::cerr;
12+
using std::endl;
13+
14+
// No public API leaves a bare Symbol (STACK_IS_SYM) on the stack -- the
15+
// interpreter pushes one during object-component access -- so the test uses the
16+
// internal push to put a Symbol there, the way nrniv would mid-expression.
17+
extern void hoc_pushs(Symbol*);
18+
19+
extern "C" void modl_reg() {}
20+
21+
static bool check(bool cond, const char* msg) {
22+
if (!cond) {
23+
cerr << "FAIL: " << msg << endl;
24+
}
25+
return cond;
26+
}
27+
28+
int main(void) {
29+
static std::array<const char*, 4> argv = {"stack_pops", "-nogui", "-nopython", nullptr};
30+
nrn_init(3, argv.data());
31+
32+
bool ok = true;
33+
34+
// Push a sentinel FIRST, below everything else the test does. The stack is
35+
// LIFO, so if every push below is matched by exactly one pop the sentinel
36+
// resurfaces on top at the end; recovering it then proves the pops neither
37+
// over-popped into what lay beneath them nor left anything behind. A
38+
// sentinel pushed *after* the pops could prove neither -- it would only ever
39+
// probe the current top.
40+
const double SENTINEL = 424242.0;
41+
nrn_double_push(SENTINEL);
42+
43+
// nrn_symbol_pop returns the Symbol on top of the stack, LIFO.
44+
Symbol* v = nrn_symbol("v");
45+
Symbol* t = nrn_symbol("t");
46+
ok &= check(v != nullptr && t != nullptr, "symbols v and t resolve");
47+
hoc_pushs(v);
48+
hoc_pushs(t);
49+
ok &= check(nrn_stack_type() == STACK_IS_SYM, "stack top is a symbol before nrn_symbol_pop");
50+
ok &= check(nrn_symbol_pop() == t, "symbol pop returns the last pushed symbol");
51+
ok &= check(nrn_symbol_pop() == v, "symbol pop returns the earlier symbol (LIFO)");
52+
53+
// nrn_object_pop returns a real object (reference-counted).
54+
Object* vec = nrn_object_new(nrn_symbol("Vector"), 0);
55+
ok &= check(vec != nullptr, "Vector constructed");
56+
nrn_object_push(vec);
57+
Object* got = nrn_object_pop();
58+
ok &= check(got == vec, "object pop returns the pushed object");
59+
if (got) {
60+
nrn_object_unref(got);
61+
}
62+
63+
// nrn_object_pop returns NULL for a nil object reference instead of
64+
// crashing -- a naive pop would dereference the NULL to take a reference and
65+
// segfault here.
66+
nrn_object_push(nullptr);
67+
ok &= check(nrn_object_pop() == nullptr, "object pop returns NULL for a nil objref");
68+
69+
// Balance: with every push above consumed by exactly one pop, the sentinel
70+
// from the very start is what remains on top.
71+
ok &= check(nrn_double_pop() == SENTINEL,
72+
"sentinel from before the pops is intact -- the stack is balanced");
73+
74+
return ok ? 0 : 1;
75+
}

0 commit comments

Comments
 (0)