Skip to content

interop: always hand a C++ bind "" for a null daslang string - #3921

Merged
aleksisch merged 2 commits into
GaijinEntertainment:masterfrom
aleksisch:fix/cast-arg-string-instantiation
Sep 2, 2026
Merged

interop: always hand a C++ bind "" for a null daslang string#3921
aleksisch merged 2 commits into
GaijinEntertainment:masterfrom
aleksisch:fix/cast-arg-string-instantiation

Conversation

@aleksisch

@aleksisch aleksisch commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

A daslang string is null when empty. Whether a bind taking const char * received that null depended on whether its translation unit included ast/ast_typefactory_bind.h, which is where the cast_arg specializations 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 for void (*)(char const *), 10 with the substitution and 17 without, and object order picks the winner. ImGui::PushID got a nullptr and crashed in ImHashStr. Nothing diagnoses it — it is an ODR violation, and ld.gold --detect-odr-violations and gcc -Wodr are both silent.

So substitute unconditionally, in all three lanes:

  • cast_arg specializations move beside the generic in simulate/interop.h; the AOT and JIT emitters stop asking whether the callee opted in, which retires needStringCast
  • the argument null-checks that can no longer fire go: module_builtin_string trades 44 stringLengthSafe for stringLength and drops 22 guards. What stays is what the substitution does not reach — a string taken by reference, non-bind helpers, and guards on lengths and FILE *
  • ast_typefactory_bind.h is left empty, so typeFactory for unbound callbacks moves next to typeFactory<TT *> in ast_typedecl.h (that order matters: above it, GCC rejects the partial specialization), cast_arg<das::string> moves to interop.h, and the header, its 102 includes and the binder line emitting it all go

Cost, from the second commit's benchmark — two binds, four const char * against four void *, same body and node: 10.3 vs 10.1 ns/op interpreted, 0 allocations either way.

Two notes for review:

  1. Removing the flag bit leaves a hole in MoreFunctionFlags, which rtti exposes and the AST module cache serializes — may want a cache version bump.
  2. Empty results still come back as null (allocateString cannot 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 "". So is_null_str in 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 — fatman and wasmboy clone their default branches.

@aleksisch aleksisch closed this Sep 1, 2026
@aleksisch aleksisch reopened this Sep 1, 2026
@aleksisch
aleksisch force-pushed the fix/cast-arg-string-instantiation branch from e2dcbd5 to 39ebd8e Compare September 1, 2026 20:10
@aleksisch aleksisch changed the title interop: keep the null string wrap for every call node instantiation interop: keep interop traits next to the templates they specialize Sep 1, 2026
@aleksisch
aleksisch force-pushed the fix/cast-arg-string-instantiation branch 2 times, most recently from 95956eb to c93f036 Compare September 1, 2026 20:29
borisbat pushed a commit to borisbat/dasImguiNodeEditor that referenced this pull request Sep 1, 2026
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
borisbat pushed a commit to borisbat/dasImguiImplot that referenced this pull request Sep 1, 2026
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
@aleksisch aleksisch changed the title interop: keep interop traits next to the templates they specialize interop: honour needStringCast in the interpreter Sep 1, 2026
@aleksisch
aleksisch force-pushed the fix/cast-arg-string-instantiation branch 2 times, most recently from cdc7830 to b155f3f Compare September 1, 2026 22:09
@aleksisch
aleksisch requested a review from borisbat September 1, 2026 22:31
@borisbat
borisbat requested a balanced review from Copilot September 1, 2026 22:59

@borisbat borisbat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

way too slow.

SimNode_StringArgNotNull ( const LineInfo & a, SimNode * se ) : SimNode(a), subexpr(se) {}
__forceinline char * compute ( Context & context ) {
DAS_PROFILE_NODE
char * res = subexpr->evalPtr(context);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we simply wrap it everywhere on das -> cpp path

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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_StringArgNotNull and wrap string arguments of needStringCast binds in sv_simulateCall, on the same predicate the AOT/JIT tiers use.
  • Delete ast_typefactory_bind.h, relocating typeFactory<ResT(*)(Args...)> into ast_typedecl.h and cast_arg<das::string> into interop.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) and tests/handle_types/string_arg_never_null.das covering 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.

@aleksisch aleksisch changed the title interop: honour needStringCast in the interpreter interop: always hand a C++ bind "" for a null daslang string Sep 2, 2026
@aleksisch
aleksisch force-pushed the fix/cast-arg-string-instantiation branch 6 times, most recently from 8ebfdec to d9751a1 Compare September 2, 2026 09:11
aleksisch added a commit to aleksisch/dasImguiImplot that referenced this pull request Sep 2, 2026
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
aleksisch added a commit to aleksisch/dasImguiNodeEditor that referenced this pull request Sep 2, 2026
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
borisbat pushed a commit to borisbat/dasImguiNodeEditor that referenced this pull request Sep 2, 2026
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
borisbat pushed a commit to borisbat/dasImguiImplot that referenced this pull request Sep 2, 2026
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
@aleksisch
aleksisch force-pushed the fix/cast-arg-string-instantiation branch 2 times, most recently from 199f177 to 6cac8a3 Compare September 2, 2026 10:10
@borisbat
borisbat self-requested a review September 2, 2026 10:15
@borisbat
borisbat dismissed their stale review September 2, 2026 10:16

changed been made

Comment thread include/daScript/simulate/interop.h Outdated
@aleksisch
aleksisch force-pushed the fix/cast-arg-string-instantiation branch 5 times, most recently from f6f04f3 to 15d9865 Compare September 2, 2026 12:36
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.
@aleksisch
aleksisch force-pushed the fix/cast-arg-string-instantiation branch from 15d9865 to c1c8b4d Compare September 2, 2026 12:42
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.
@aleksisch
aleksisch force-pushed the fix/cast-arg-string-instantiation branch from c1c8b4d to f11baa3 Compare September 2, 2026 12:53
@aleksisch
aleksisch merged commit 5c734c4 into GaijinEntertainment:master Sep 2, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants