interop: always hand a C++ bind "" for a null daslang string - #3921
Conversation
e2dcbd5 to
39ebd8e
Compare
95956eb to
c93f036
Compare
daScript deletes that header: the cast_arg specializations it carried move next to the generic cast_arg in simulate/interop.h, and the typeFactory callback fallback next to the primary typeFactory in ast/ast_typedecl.h. The binder no longer emits the include. The wrap had to move because the call node is now keyed on the signature, so one instantiation is shared by binds from many TUs, and only the TUs including this header saw the null-string conversion. Needs GaijinEntertainment/daScript#3921
daScript deletes that header: the cast_arg specializations it carried move next to the generic cast_arg in simulate/interop.h, and the typeFactory callback fallback next to the primary typeFactory in ast/ast_typedecl.h. The binder no longer emits the include. The wrap had to move because the call node is now keyed on the signature, so one instantiation is shared by binds from many TUs, and only the TUs including this header saw the null-string conversion. Needs GaijinEntertainment/daScript#3921
cdc7830 to
b155f3f
Compare
| SimNode_StringArgNotNull ( const LineInfo & a, SimNode * se ) : SimNode(a), subexpr(se) {} | ||
| __forceinline char * compute ( Context & context ) { | ||
| DAS_PROFILE_NODE | ||
| char * res = subexpr->evalPtr(context); |
There was a problem hiding this comment.
this is going to be slower than original implementation, which uses cast<>
cast gets inlined on C++ side, this creates interpreter node - which is a lot slower
even blanket cast_arg<char *> would be faster
There was a problem hiding this comment.
Now we simply wrap it everywhere on das -> cpp path
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core interop null-string semantics for every string bind across the engine and removes a public installed header, a high-impact cross-cutting change that warrants final human review despite being well-tested.
Pull request overview
This PR moves the "null daScript string → empty C string" conversion for interop binds from a header-wide, per-TU mechanism into the interpreter's simulate path, keyed on the bind's needStringCast flag — matching how the AOT (das_string_cast in aot_cpp.das) and JIT (llvm_jit.das) backends already behave. Previously the conversion lived in cast_arg<char*>/cast_arg<const char*> specializations inside ast_typefactory_bind.h, which applied to every bind in any TU that happened to include that header and to none elsewhere — an unstable, link-order-dependent behavior (the motivating ImGui::PushID crash). The header is deleted and its still-needed pieces relocated.
Changes:
- Add
SimNode_StringArgNotNulland wrap string arguments ofneedStringCastbinds insv_simulateCall, on the same predicate the AOT/JIT tiers use. - Delete
ast_typefactory_bind.h, relocatingtypeFactory<ResT(*)(Args...)>intoast_typedecl.handcast_arg<das::string>intointerop.h; drop its include from ~90 module/tutorial/example TUs, the cbind generator, and the install list. - Add a UnitTest bind pair (
test_string_arg_length/_cast) andtests/handle_types/string_arg_never_null.dascovering both halves.
File summaries
| File | Description |
|---|---|
include/daScript/simulate/simulate_nodes.h |
New SimNode_StringArgNotNull node (null → ""). |
src/simulate/simulate_visit.cpp |
Visitor plumbing for the new node (matches KeepAlive). |
src/ast/ast_simulate.cpp |
Wraps needStringCast string args in sv_simulateCall. |
include/daScript/simulate/interop.h |
Adds relocated cast_arg<das::string>. |
include/daScript/ast/ast_typedecl.h |
Adds relocated typeFactory<ResT(*)(Args...)>. |
include/daScript/ast/ast_typefactory_bind.h |
Deleted (specializations relocated / dropped). |
modules/dasUnitTest/test_handles.cpp |
Adds testStringArgLength bound with and without needStringCast. |
tests/handle_types/string_arg_never_null.das |
New test covering cast, non-cast, empty, null, non-empty. |
CMakeLists.txt |
Removes deleted header from release AST include list. |
modules/dasClangBind/cbind/cbind_boost.das |
Stops emitting the deleted header in generated binds. |
~90 module/tutorial/example .cpp files |
Drop the now-unused header include. |
One minor, non-blocking note (not filable — the file isn't in the diff): tests/README.md asks that every .das under tests/ be listed, but its handle_types/ section is already stale (missing several files); consider adding string_arg_never_null.das there.
Review details
- Files reviewed: 112/112 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
8ebfdec to
d9751a1
Compare
daScript substitutes "" for a null daslang string on every bind now, so needStringCast is gone and the fixup which set it goes with it. ast_typefactory_bind.h is gone too: the cast_arg specializations it carried moved next to the generic cast_arg in simulate/interop.h, and the typeFactory callback fallback next to the primary typeFactory in ast/ast_typedecl.h. The binder no longer emits the include, so this is what regenerating produces. Needs GaijinEntertainment/daScript#3921
daScript substitutes "" for a null daslang string on every bind now, so needStringCast is gone and the fixup which set it goes with it. ast_typefactory_bind.h is gone too: the cast_arg specializations it carried moved next to the generic cast_arg in simulate/interop.h, and the typeFactory callback fallback next to the primary typeFactory in ast/ast_typedecl.h. The binder no longer emits the include, so this is what regenerating produces. Needs GaijinEntertainment/daScript#3921
daScript substitutes "" for a null daslang string on every bind now - interpreter, AOT and JIT alike - so Function::needStringCast is gone and the scan which set it has nothing left to do. The float2/float4 by-value fixup in the same loop stays. Needs GaijinEntertainment/daScript#3921
daScript substitutes "" for a null daslang string on every bind now - interpreter, AOT and JIT alike - so Function::needStringCast is gone and the scan which set it has nothing left to do. The float2/float4 by-value fixup in the same loop stays. Needs GaijinEntertainment/daScript#3921
199f177 to
6cac8a3
Compare
f6f04f3 to
15d9865
Compare
A daslang string is null when empty, and a bind taking const char * used to receive that null - or not - depending on whether its translation unit included ast/ast_typefactory_bind.h, where the cast_arg specializations that substitute "" lived. That was survivable while SimNode_ExtFuncCall took the function as a non type template parameter: the node was instantiated once per bound function, in the unit that did the binding, so the choice was fixed per bind. Keyed on the signature alone, one node serves every bind of that signature. A game link has 27 objects defining the node for void (*)(char const *): 10 generated dasImgui units which include the header, and 17 engine units which do not. Same weak symbol, different bodies, so which definition survives depends on object order. ImGui::PushID got a nullptr and crashed in ImHashStr. It is an ODR violation, and none of it is diagnosed: neither ld.gold --detect-odr-violations nor gcc -Wodr says a word. Substitute unconditionally instead, in all three lanes: the cast_arg specializations move next to the generic in simulate/interop.h, and the AOT and JIT emitters stop asking whether the callee opted in. The AOT emitter opened the cast on the argument expression type and closed it on the declared parameter type, which only stayed balanced while the flag gated both - it now reads the declared type on both sides. That retires needStringCast, its rtti flag name, its printer and its setters. The bit leaves a hole in MoreFunctionFlags, which rtti exposes and the AST module cache serializes. A bind never sees a null string now, so the null checks guarding those arguments go: module_builtin_string trades 44 stringLengthSafe calls for stringLength and drops 22 guards, among them the one which made ends_with(das_string, "") disagree with ends_with(string, ""), which two pins recorded. What stays is what the substitution does not reach - a string taken by reference (delete_string), internal helpers which are not binds, and guards on lengths and FILE handles. That leaves ast_typefactory_bind.h with nothing worth keeping: the typeFactory fallback for unbound callbacks moves next to typeFactory<TT *> in ast_typedecl.h, where its non-dependent use of typeFactory<void *> resolves, and the header goes - with its include in every binding unit, and the line which emitted it in the C++ binder. cast_arg<das::string> goes with it rather than moving: it is what let a bind take das::string by value, which nothing does, and which AOT could not emit anyway since there is no cast<das::string>. Empty results still come back as null, since allocateString cannot allocate an empty string. But a null which crosses into a bind and back, as it does through _temp_string_result, returns as "". The byte view pins watched that pointer through unsafe(reinterpret), so is_null_str measures length instead, and the one assertion which asked whether a NUL holding run was allocated at all is gone - das cannot tell any more. tests/handle_types/string_arg_never_null covers the argument path: a null string, an empty literal and a non-empty string, through a bind which reports what it received. llvm_jit_run.das holds the emitter source pin and the codegen version; both move, since the JIT substitutes for every string argument now. The flag bit leaves a hole in MoreFunctionFlags, which the AST module cache serializes, so AstSerializer::getVersion moves - to 199, which is where the dagor copy already sits, so that line stops being a local patch there.
15d9865 to
c1c8b4d
Compare
test_four_strings takes four const char *, test_four_pointers takes four void *, and both xor their four arguments, so the pair shares a node shape, an argument count and a body - only the substitution differs. Interpreted, the strings side runs 0.2 to 0.4 ns/op slower across six sample pairs, against a call which spends ten to fifteen ns in dispatch alone - one test and one cmov per argument, absorbed. The absolute figures move with machine load; the ordering did not. Both report 0 B/op and 0 strings/op, so nothing allocates behind the measurement.
c1c8b4d to
f11baa3
Compare
A daslang string is null when empty. Whether a bind taking
const char *received that null depended on whether its translation unit includedast/ast_typefactory_bind.h, which is where thecast_argspecializations substituting""lived — fine while the call node was per bound function, broken once it became per signature: one game link has 27 objects defining the node forvoid (*)(char const *), 10 with the substitution and 17 without, and object order picks the winner.ImGui::PushIDgot anullptrand crashed inImHashStr. Nothing diagnoses it — it is an ODR violation, andld.gold --detect-odr-violationsandgcc -Wodrare both silent.So substitute unconditionally, in all three lanes:
cast_argspecializations move beside the generic insimulate/interop.h; the AOT and JIT emitters stop asking whether the callee opted in, which retiresneedStringCastmodule_builtin_stringtrades 44stringLengthSafeforstringLengthand drops 22 guards. What stays is what the substitution does not reach — a string taken by reference, non-bind helpers, and guards on lengths andFILE *ast_typefactory_bind.his left empty, sotypeFactoryfor unbound callbacks moves next totypeFactory<TT *>inast_typedecl.h(that order matters: above it, GCC rejects the partial specialization),cast_arg<das::string>moves tointerop.h, and the header, its 102 includes and the binder line emitting it all goCost, from the second commit's benchmark — two binds, four
const char *against fourvoid *, same body and node: 10.3 vs 10.1 ns/op interpreted, 0 allocations either way.Two notes for review:
MoreFunctionFlags, which rtti exposes and the AST module cache serializes — may want a cache version bump.allocateStringcannot allocate an empty string), but a null crossing into a bind and back — as through_temp_string_result, wrapped around every[temp_string_result]builtin — returns as"". Sois_null_strin the byte-view pins measures length now, and one assertion asking whether a NUL-holding run was allocated at all is gone: das can no longer tell. Covering that again needs a C++-side test calling the builtin directly.Out-of-tree generated bindings include the deleted header, so this needs borisbat/dasImguiImplot#30 and borisbat/dasImguiNodeEditor#46 first —
fatmanandwasmboyclone their default branches.