Skip to content

Commit f5ccbdb

Browse files
authored
neuronapi: add nrn_object_new_nothrow for error-reporting construction (#3831)
nrn_object_new calls hoc_newobj1, which throws a C++ exception on a constructor error (bad arguments, a failing INITIAL, ...). That is unsafe for a non-C++ caller (ctypes, MATLAB) where the exception would cross the FFI boundary. nrn_object_new_nothrow catches the error and reports it via a return code + error buffer, the constructor counterpart of nrn_function_call_nothrow / nrn_method_call_nothrow. Construction runs under a new OcJump::newobj_throw_on_exception helper that restores the interpreter stack on failure, mirroring execute_throw_on_exception.
1 parent 8d7d617 commit f5ccbdb

7 files changed

Lines changed: 143 additions & 0 deletions

File tree

docs/capi.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,29 @@ Functions, objects, and the stack
973973
:c:func:`nrn_object_new`,
974974
:c:func:`nrn_object_ref`
975975

976+
.. c:function:: int nrn_object_new_nothrow(Symbol* sym, int narg, Object** result, char* error_msg, size_t error_msg_size)
977+
978+
Create a new object, reporting a constructor error instead of throwing.
979+
980+
:param sym: Symbol representing the object class/type.
981+
:param narg: Number of constructor arguments on the stack.
982+
:param result: Set to the new object on success, or ``NULL`` on error.
983+
:param error_msg: Buffer filled with a message on error (may be ``NULL``).
984+
:param error_msg_size: Size of ``error_msg``.
985+
:returns: 0 on success, nonzero if the HOC constructor errored.
986+
987+
Like :c:func:`nrn_object_new`, but a constructor error (bad arguments, a
988+
failing ``INITIAL``, etc.) is caught and reported rather than thrown as a
989+
C++ exception, so a non-C++ caller (ctypes, MATLAB, ...) does not have an
990+
exception cross the call boundary. This is the constructor counterpart of
991+
:c:func:`nrn_function_call_nothrow` and :c:func:`nrn_method_call_nothrow`.
992+
The interpreter stack is restored if construction fails.
993+
994+
.. seealso::
995+
996+
:c:func:`nrn_object_new`,
997+
:c:func:`nrn_function_call_nothrow`
998+
976999
.. c:function:: Symbol* nrn_method_symbol(const Object* obj, const char* name)
9771000
9781001
Get a method symbol from an object by name.

src/nrniv/neuronapi.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,43 @@ Object* nrn_object_new_wrap(Symbol* sym, void* cpp_object) {
394394
return hoc_new_object(sym, cpp_object);
395395
}
396396

397+
int nrn_object_new_nothrow(Symbol* sym,
398+
int narg,
399+
Object** result,
400+
char* error_msg,
401+
size_t error_msg_size) {
402+
// Like nrn_object_new, but a HOC constructor error (bad arguments, a failing
403+
// INITIAL, etc.) is caught and reported instead of thrown, so a non-C++
404+
// caller (ctypes, MATLAB, ...) does not have a C++ exception propagate
405+
// across the FFI boundary. On success returns 0 with *result set; on error
406+
// returns nonzero with *result NULL and error_msg populated.
407+
if (error_msg && error_msg_size > 0) {
408+
error_msg[0] = '\0';
409+
}
410+
if (result) {
411+
*result = nullptr;
412+
}
413+
try {
414+
Object* obj = OcJump::newobj_throw_on_exception(sym, narg);
415+
if (result) {
416+
*result = obj;
417+
}
418+
return 0;
419+
} catch (const std::exception& e) {
420+
if (error_msg && error_msg_size > 0) {
421+
strncpy(error_msg, e.what(), error_msg_size - 1);
422+
error_msg[error_msg_size - 1] = '\0';
423+
}
424+
return 1;
425+
} catch (...) {
426+
if (error_msg && error_msg_size > 0) {
427+
strncpy(error_msg, "Unknown exception occurred", error_msg_size - 1);
428+
error_msg[error_msg_size - 1] = '\0';
429+
}
430+
return 1;
431+
}
432+
}
433+
397434
Symbol* nrn_method_symbol(const Object* obj, char const* const name) {
398435
return hoc_table_lookup(name, obj->ctemplate->symtable);
399436
}

src/nrniv/neuronapi.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ nrn_stack_types_t nrn_stack_type(void);
9696
char const* nrn_stack_type_name(nrn_stack_types_t id);
9797
Object* nrn_object_new(Symbol* sym, int narg);
9898
Object* nrn_object_new_wrap(Symbol* sym, void* cpp_object);
99+
int nrn_object_new_nothrow(Symbol* sym,
100+
int narg,
101+
Object** result,
102+
char* error_msg,
103+
size_t error_msg_size);
99104
Symbol* nrn_method_symbol(const Object* obj, const char* name);
100105
// TODO: the next two functions throw exceptions in C++; need a version that
101106
// returns a bool success indicator instead (this is actually the

src/nrniv/ocjump.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,22 @@ void OcJump::execute_throw_on_exception(Symbol* sym, int narg) {
154154
}
155155
}
156156

157+
extern Object* hoc_newobj1(Symbol*, int);
158+
159+
Object* OcJump::newobj_throw_on_exception(Symbol* sym, int narg) {
160+
// Construct an object, restoring interpreter state (including the stack) if
161+
// the HOC constructor errors, so a caller can catch the exception without
162+
// leaving the stack dirty. Same pattern as execute_throw_on_exception.
163+
saved_state before{};
164+
try_catch_depth_increment tell_children_we_will_catch{};
165+
try {
166+
return hoc_newobj1(sym, narg);
167+
} catch (...) {
168+
before.restore();
169+
throw;
170+
}
171+
}
172+
157173
void* OcJump::fpycall(void* (*f)(void*, void*), void* a, void* b) {
158174
saved_state before{};
159175
try_catch_depth_increment tell_children_we_will_catch{};

src/nrniv/ocjump.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ struct OcJump {
4343
static void* fpycall(void* (*) (void*, void*), void*, void*);
4444
static void execute_throw_on_exception(Symbol* sym, int narg);
4545
static void execute_throw_on_exception(Object* obj, Symbol* sym, int narg);
46+
static Object* newobj_throw_on_exception(Symbol* sym, int narg);
4647
};

test/api/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ foreach(
44
hh_sim.cpp
55
netcon.cpp
66
node_index.cpp
7+
object_new_nothrow.cpp
78
object_new_wrap.cpp
89
segment_diam.cpp
910
sections.cpp

test/api/object_new_nothrow.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// NOTE: this assumes neuronapi.h is on your CPLUS_INCLUDE_PATH
2+
// Exercises nrn_object_new_nothrow, which constructs an object like
3+
// nrn_object_new but reports a HOC constructor error through a return code and
4+
// error buffer instead of letting a C++ exception cross the call boundary.
5+
#include <array>
6+
#include <iostream>
7+
#include "neuronapi.h"
8+
9+
using std::cerr;
10+
using std::endl;
11+
12+
extern "C" void modl_reg(){/* No modl_reg */};
13+
14+
static bool check(bool cond, const char* msg) {
15+
if (!cond) {
16+
cerr << "FAIL: " << msg << endl;
17+
}
18+
return cond;
19+
}
20+
21+
int main(void) {
22+
static std::array<const char*, 4> argv = {"object_new_nothrow", "-nogui", "-nopython", nullptr};
23+
nrn_init(3, argv.data());
24+
25+
bool ok = true;
26+
char err[256];
27+
28+
// Success path: a Vector(3) constructs, returns 0, and fills *result.
29+
Object* v = nullptr;
30+
nrn_double_push(3);
31+
int rc = nrn_object_new_nothrow(nrn_symbol("Vector"), 1, &v, err, sizeof(err));
32+
ok &= check(rc == 0, "successful construction returns 0");
33+
ok &= check(v != nullptr, "successful construction sets *result");
34+
ok &= check(v != nullptr && nrn_vector_capacity(v) == 3, "constructed the requested Vector(3)");
35+
ok &= check(err[0] == '\0', "error buffer stays empty on success");
36+
37+
// Failure path: a template whose init calls execerror. nrn_object_new would
38+
// throw; nrn_object_new_nothrow must catch it.
39+
nrn_hoc_call("begintemplate Boom\nproc init() { execerror(\"boom\", \"\") }\nendtemplate Boom");
40+
Object* b = reinterpret_cast<Object*>(0x1); // sentinel; must be nulled
41+
rc = nrn_object_new_nothrow(nrn_symbol("Boom"), 0, &b, err, sizeof(err));
42+
ok &= check(rc != 0, "a failing constructor returns nonzero");
43+
ok &= check(b == nullptr, "*result is NULL after a failed construction");
44+
ok &= check(err[0] != '\0', "error buffer is populated on failure");
45+
46+
// A failed construction must not corrupt the stack: a value pushed after it
47+
// pops back cleanly.
48+
nrn_double_push(42.0);
49+
ok &= check(nrn_double_pop() == 42.0, "stack is intact after a failed construction");
50+
51+
// A NULL error buffer is tolerated on both paths.
52+
Object* v2 = nullptr;
53+
rc = nrn_object_new_nothrow(nrn_symbol("Vector"), 0, &v2, nullptr, 0);
54+
ok &= check(rc == 0 && v2 != nullptr, "NULL error buffer tolerated on success");
55+
Object* b2 = nullptr;
56+
rc = nrn_object_new_nothrow(nrn_symbol("Boom"), 0, &b2, nullptr, 0);
57+
ok &= check(rc != 0 && b2 == nullptr, "NULL error buffer tolerated on failure");
58+
59+
return ok ? 0 : 1;
60+
}

0 commit comments

Comments
 (0)