Skip to content

Disengaged safeguards appear to still check bind / bind_mut #1704

Open

Description

@caphindsight

I was experimenting with making multiple threads work with the same node by removing experimental-threads, and using safeguards-release-disengaged instead to prevent the bind guard interior mutability checker from complaining, however, I noticed that safeguards-release-disengaged appears to be broken?

Cargo.toml:

[package]
name = "gdext_rust"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]
path = "src/gdext_rust.rs"

[dependencies.godot]
version = "0.5.5"
features = ["api-4-7", "safeguards-release-disengaged", "register-docs"]

lib.rs:

use godot::prelude::*;

struct GdextRustExtLib;

#[gdextension]
unsafe impl ExtensionLibrary for GdextRustExtLib {}

#[derive(GodotClass)]
#[class(base=Node, init)]
struct RustNode {
  #[base]
  base: Base<Node>,

  #[var]
  ans: i64,
}

#[godot_api]
impl RustNode {
  fn is_prime(x: i64) -> bool {
    let mut y: i64 = 2;
    while y * y <= x {
      if x % y == 0 {
        return false;
      }
      y += 1;
    }
    true
  }

  #[func]
  fn compute_answer(&mut self, #[opt(default=100_000_000_i64)] n: i64) {
    let mut ans: i64 = 0;
    for i in 2_i64 .. n {
      if RustNode::is_prime(i) {
        ans += 1;
      }
    }
    self.base_mut().set_deferred("ans", &ans.to_variant());
  }
}

main.gd:

extends Node

@onready var rust_node: RustNode = $RustNode
var counter: int = 0
var task_id: int = -1
var task_state: String = "running"

func _ready() -> void:
	task_id = WorkerThreadPool.add_task(rust_node.compute_answer.bind(10_000_000))

func _process(_delta: float) -> void:
	counter += 1
	if task_id != -1 and WorkerThreadPool.is_task_completed(task_id):
		WorkerThreadPool.wait_for_task_completion(task_id)
		task_state = "completed"
		task_id = -1
	print("Frame #", counter, "; answer is: ", rust_node.ans, "; ", task_state)

I expected the safeguard checks to not trigger due to the safeguards-release-disengaged option in Cargo.toml (I'm building with cargo build --release).

However, I'm seeing failed checks in the logs:

E 0:00:00:620   main.gd:17 @ _process(): [panic .../godot-core-0.5.5/src/storage/mod.rs:26]
  Gd<T>::bind() failed, already bound; T = gdext_rust::RustNode.
    Make sure to use `self.base_mut()` or `self.base()` instead of `self.to_gd()` when possible.
    Details: cannot borrow while accessible mutable borrow exists.
  <C++ Source> .../godot-core-0.5.5/src/private.rs:331 @ godot_core::private::install_godot_rust_hook::{{closure}}()
  <Stack Trace> main.gd:17 @ _process()

Activity

  1. Bromeon commented on Sep 18, 2026

    @Bromeon
    Member

    bind/bind_mut is something to potentially optimize, but there seems to be a misunderstanding:

    using safeguards-release-disengaged instead to prevent the bind guard interior mutability checker from complaining, however, I noticed that safeguards-release-disengaged appears to be broken?

    This flag is a pure optimization, you cannot rely on it for semantic reasons. Especially not because that usually means your code becomes unsound.

  2. added
    questionNot a problem with the library, but a question regarding usage.
    on Sep 18, 2026
  3. caphindsight commented on Sep 18, 2026

    @caphindsight
    ContributorAuthor

    This flag is a pure optimization, you cannot rely on it for semantic reasons.

    Is it guaranteed to be optimized out? Because it seems like it doesn't work?

  4. Bromeon commented on Sep 18, 2026

    @Bromeon
    Member

    No:

    • there's no guarantee anything is optimized out, it's best-effort and may change over time
    • if your logic relies on checks being absent, your code is unsound
  5. Bromeon commented on Sep 18, 2026

    @Bromeon
    Member

    Regarding threading, it's a known issue for a long time. But experimental-threads is already a big hack, and we shouldn't encourage even more unsound code just to shut up the compiler, because all it will achieve is UB at runtime 🫠

    In case you have time to devote to this topic, PR #1524 is a start and could use some input, or maybe you have other ideas. But I also totally understand if you don't want to deal with this and just get your game done 😀 unfortunately Godot really doesn't make it easy for Rust as it gives almost no guarantees about thread safety.

  6. caphindsight commented on Sep 18, 2026

    @caphindsight
    ContributorAuthor

    and we shouldn't encourage even more unsound code just to shut up the compiler, because all it will achieve is UB at runtime

    Can you elaborate why this is so?

    Is your point that this can be unsound, or that it is necessarily unsound?

    I 100% agree with the former, but I cannot see a convincing argument for the latter. I can, for example, envision completely sound code that runs the following function on the thread:

    /// Pass me to WorkerThreadPool.add_task!
    #[func]
    fn run_expensive_computation(this: Gd<Self>) {
      let result = ... // Run some expensive computation that doesn't touch this.
      this.call_deferred("report_result", result);
    }
    

    What is the current behaviour with/without experimental-threads here? Would it allow running multiple such functions in parallel?
    From experimenting in the past days, my understanding is: no, it would not.

    Would it not be sound to allow multiple of these functions running on worker threads?

    The problem is, for me, that I absolutely love this crate, and nothing would make me happier than to use it in an actual game, but I have encountered a hard block: can't run rust code on Godot's worker threads. As small as this blocker sounds, it is a make or break... It would be a shame if this meant I can't use this crate, I am trying to find a way forward...

  7. TitanNano commented on Sep 18, 2026

    @TitanNano
    Contributor

    I have encountered a hard block: can't run rust code on Godot's worker threads. As small as this blocker sounds, it is a make or break...

    I don't know your exact setup, and it's not a direct solution to the stated problem, but have you considered running your code on a Rust thread?

  8. caphindsight commented on Sep 18, 2026

    @caphindsight
    ContributorAuthor

    I don't know your exact setup, and it's not a direct solution to the stated problem, but have you considered running your code on a Rust thread?

    I have. In my setup, I have a virtual function that is overwritten in GDScript, and it has to be called in the worker threads (it's procedural generation, essentially; the procedural engine is written in Rust, and calls into GDScript callbacks that define properties of specific assets). I find that the generated bind / thread guards prevent me from calling the virtual function in parallel, it doesn't matter if it happens from a Rust or Godot thread.

  9. caphindsight commented on Sep 18, 2026

    @caphindsight
    ContributorAuthor

    we shouldn't encourage even more unsound code just to shut up the compiler, because all it will achieve is UB at runtime

    May I offer a different perspective, perhaps?

    There's a reason unsafe is a core part of Rust: you can't do certain things, especially if performance is at stake, without it.
    Rust's model, the way I see it, is "use safe Rust whenever possible, use unsafe whenever it isn't".
    Even swapping two elements in a slice can't be done without unsafe :)

    So what I'm suggesting here is to have two things: an unsafe optout of the safeguards, a "use it at your own risk" kind of thing; and a safe threading design (which will probably come after, since it's hard to do) that potentially only supports a limited set of use cases.

    My project would essentially only have a few places where I'd opt out of these checks. I would have to carefully check and test those places, but if it's only a few, it's a manageable amount. The rest of the codebase is still benefiting from safety.

    I don't think method-level or class-level optouts from the bind guard / thread checker are a bad idea at all.

  10. Bromeon commented on Sep 18, 2026

    @Bromeon
    Member

    Can you elaborate why this is so?

    Is your point that this can be unsound, or that it is necessarily unsound?

    Gd::bind_mut's interior mutability works like RefCell::borrow_mut: ensure at runtime that only a single &mut T ever exists.

    In your initial post, you wrote:

    prevent the bind guard interior mutability checker from complaining

    implying that you ran into a panic.

    This panic isn't "complaining" to annoy you, it means "you would at this point obtain a second &mut T". This is always undefined behavior in Rust, no matter what you do with the resulting reference. Whether it appears to work is completely irrelevant -- the compiler can assume that this never happens and do according optimizations in the LLVM backend (such as restrict pointers, instruction reordering, etc).

    Unsafe vs. unsound, see this page:

    unsafe only means that avoiding undefined behavior is on the programmer; it does not change anything about the fact that Rust programs must never cause undefined behavior.

    So if you ever have a situation where the dynamic borrow-check panics, it means that removing that check would cause UB. So you can't argue that the panic holds you back from achieving something, because the alternative is an mal-formed (aka unsound or buggy) program. I do see the performance PoV, but that's a very different line of reasoning 🙂


    My project would essentially only have a few places where I'd opt out of these checks. I would have to carefully check and test those places, but if it's only a few, it's a manageable amount.

    Out of curiosity, how would you test this?

  11. caphindsight commented on Sep 18, 2026

    @caphindsight
    ContributorAuthor

    I'm arguing multiple things here and I fear they got mixed up. I do have opinions about threading, and there's another issue I opened at about the same time where I argue that a static function that doesn't bind the object (for example only calling call_deferred on the Gd) should be allowed to bypass guards.

    I am not arguing that I should be selectively allowed to bypass bind guards by having two binds at the same time.

    What I am arguing in this issue is simply that the stated behaviour "safeguards are stripped out from builds that use the disengage feature" does not seem to correspond to reality.

    For me, this is a problem only because I have no way of not paying the performance penalty of checking the guards at runtime.

    So, for example, I am not at all arguing for removing the bind_mut checker in debug builds.

    You may argue that the way I arrived at this is wrong, since it took writing code that is UB. You would be correct. Technically, you're allowed to add the guards back in since it's undefined behaviour anyway. But in reality, we both know that the guards would still be checked if there was no undefined behaviour in my code, too. So adding the UB is still a valid way to spot the existence of checks, in practice (another way would be to inspect the ASM by hand).

    Again, to reiterate. This issue is about the "disengage" crate feature not disengaging everything in release builds. I argue it should disengage everything, because I want my game to be fast. Not because I want to bypass it.

    I have argued for allowing bypassing here only because there's a separate issue I have open at the same time, and it's easy to confuse both points.

  12. caphindsight commented on Sep 18, 2026

    @caphindsight
    ContributorAuthor

    W.r.t. the other point about allowing to bypass bind guards, I wonder if something like this would work as a way to bypass them:

    #[func(unsafe)]
    unsafe fn my_parallel_method(self: *mut MyNode) {
      // I promise to use self correctly.
    }
  13. Bromeon commented on Sep 19, 2026

    @Bromeon
    Member

    I'm arguing multiple things here and I fear they got mixed up.

    Your initial post was pretty clear on "using safeguards-release-disengaged instead to prevent the bind guard interior mutability checker from complaining" and "I expected the safeguard checks to not trigger", so you simply didn't argue about performance.
    But you do now, fair.


    I do have opinions about threading, and there's another issue I opened at about the same time where I argue that a static function that doesn't bind the object (for example only calling call_deferred on the Gd) should be allowed to bypass guards.

    #1693, yes and I already answered that it makes sense 🙂


    For me, this is a problem only because I have no way of not paying the performance penalty of checking the guards at runtime.

    I wonder how big the impact of the guards is, compared to e.g. locking on Godot side and FFI/marshaling overhead. Did you do any measurements here? We also have a #[bench] infrastructure in godot-rust itself, but it might be more meaningful in the context of a frame of your game.


    Also, this is unanswered (genuine question of mine, I'm not trying to test you or anything):

    My project would essentially only have a few places where I'd opt out of these checks. I would have to carefully check and test those places, but if it's only a few, it's a manageable amount.

    Out of curiosity, how would you test this?

  14. caphindsight commented on Sep 19, 2026

    @caphindsight
    ContributorAuthor

    II> Your initial post was pretty clear on "using safeguards-release-disengaged instead to prevent the bind guard interior mutability checker from complaining" and "I expected the safeguard checks to not trigger", so you simply didn't argue about performance.
    But you do now, fair.

    Yes, I said I have experimented with this and found it to also be broken. Despite what some Rust developers may believe, experimenting with code that has an UB in it is not illegal in most jurisdictions.

    I expected the safeguard checks to not trigger

    This was my main argument from the start. I assumed that an option that says safeguards-release-disengaged should, well, disengage the safeguards in release builds, but I observed that it is apparently not so, contrary to what the documentation claims. What goals I may have been pursuing while I discovered this is irrelevant to the issue.

    I wonder how big the impact of the guards is, compared to e.g. locking on Godot side and FFI/marshaling overhead. Did you do any measurements here? We also have a #[bench] infrastructure in godot-rust itself, but it might be more meaningful in the context of a frame of your game.

    I don't have the raw numbers yet.

    Also, this is unanswered (genuine question of mine, I'm not trying to test you or anything):

    It's a long, complicated answer. What would you do if you wrote the whole thing in C++? You'd inspect the code manually, looking at the most likely places it could fail, then playtest the build to iron out any crashes. There's ASAN / UBSAN / etc., your runtime borrow checker would help here too, just not shipped as part of the final game.

  15. Bromeon commented on Sep 19, 2026

    @Bromeon
    Member

    What goals I may have been pursuing while I discovered this is irrelevant to the issue.

    It actually isn't, one of my tasks is to identify XY problems. And indeed, the report "I can't avoid safeguard panics" turned out to mean "I want to get more performance out of this". Given that a significant number of issues reported here is about misunderstandings of borrowing, UB and safety in general, I don't think it's inappropriate to double-check that a request has the right motivation.


    I don't have the raw numbers yet.

    If you have them, please share. I can do benchmarks in godot-rust itself of course, but seeing a more real-world use case would be interesting.

    You're right that the docs are imprecise; safeguards-disengaged doesn't guarantee that everything is disabled (it's not practically possible in Rust, and Godot has its own checks), and especially not that it enables different semantics (such as avoiding a panic that would otherwise be hit). This is the main risk I see -- that people misunderstand it and introduce bugs because they believe "ah, now it works". But if we know more about the performance, we can evaluate more angles.

    We would also need to consider how to disable checks for bind/bind_mut... unwrap_unchecked of inner cell? Direct raw pointer?

    (P.s. sorry edited wrong post, reverted)

  16. caphindsight commented on Sep 22, 2026

    @caphindsight
    ContributorAuthor

    Brief comment on the performance aspect. I never got to the part where I can measure performance because of some locking issues with experimental-threads. The procedural generator I have written using this crate frequently drops frames when new chunks load in.

    For comparison, I'm getting stable 240 FPS with the equivalent written in GDScript. Not like it's a fair comparison — the performance bottleneck is clearly the main thread waiting on the mutex during multithreaded calls which should be safe to do without the mutex.

    I think fixing the static function issue I mentioned in the other thread (no pun intended) here will help with the FPS drops.

    Currently I have GDScript code outperforming Rust by all metrics. And it was much faster to write, which is, of course, expected. Not a good start :(

  17. TitanNano commented on Sep 22, 2026

    @TitanNano
    Contributor

    Currently I have GDScript code outperforming Rust by all metrics. And it was much faster to write, which is, of course, expected. Not a good start :(

    Generally, I would recommend avoiding as much GDScript / engine interaction as possible when you want to achieve maximum performance.

    You can, for example, gather all the necessary data into a Rust struct up-front and then run your multithreaded logic on a Rust thread pool with e.g. rayon.

  18. caphindsight commented on Sep 22, 2026

    @caphindsight
    ContributorAuthor

    Generally, I would recommend avoiding as much GDScript / engine interaction as possible when you want to achieve maximum performance.

    I am trying to achieve a balanced solution between performance and development speed. So from my Rust code that runs the generator I would call into script virtual functions that return specific properties that tell the generation engine how to place assets. I understand that this has a performance hit, but with this design I can iterate on levels a lot faster.

    Generally, I am using Godot for development, so "avoid engine interactions" is not an answer I can accept.

  19. Bromeon commented on Sep 23, 2026

    @Bromeon
    Member

    Generally, I am using Godot for development, so "avoid engine interactions" is not an answer I can accept.

    You conveniently left out "as much as possible" in his quote. Also, the meaning is obviously not to avoid Godot, but that batched data exchange works better than thousands of individually spread out FFI calls, simply because there's a ceiling due to Godot's own per-call overhead. Of course, that doesn't excuse unoptimized paths on godot-rust side, it was merely a suggestion on how to deal with the status quo before anything is fixed 🙂

    To make things faster, let's focus on technical facts. Was the slowness you encountered under safeguards-disengaged feature and Rust --release builds? What order of magnitude are we talking -- you mention "240 FPS in GDScript but drops frames in Rust", which is very vague. Any chance we can isolate the offenders more clearly, e.g. in a minimal example? That would allow me to do more precise profiling/benchmarking. Ideal would be something we can run in godot-rust's #[bench] infrastructure (it just calls a function many times and measures it) -- I've been able to optimize a ton of the library using that approach.

  20. caphindsight commented on Sep 23, 2026

    @caphindsight
    ContributorAuthor

    I was trying to evaluate the performance on a realistic workload. As I'm sure you're aware, benchmarking a small line of code in a tight loop is not always a good indicator of how your game will be running. You won't catch various cache misses, branch mispredictions, etc. which are likely to happen in production workloads which are not just focused on spinning a tiny block of code over and over again.

    I can put together a benchmark too, if that helps you. I'm not sure I see the point: clearly, running checks is less performant than not running checks. I believe in empowering the user to do what they want while still providing safety checks by default, that seems to also be the general Rust philosophy, I was surprised that the disengaged level of checks doesn't fully disable them. That's why I filed this issue - I honestly thought this was just a bug, not a deliberate design decision.

    you mention "240 FPS in GDScript but drops frames in Rust", which is very vague

    Yeah, I know it's vague, I didn't mean to report those numbers like they mean anything. I was describing my setup, so you better understand what it is that I'm trying to do. You conveniently left out the part where I explained how the reason I couldn't get a good handle on performance (like e.g. by running a profiler, like Very Sleepy, on a working prototype), is that I could never resolve the threading issues which I am convinced arise from grabbing exclusive locks in the generated bindings. If I could avoid grabbing those locks, I would have the real performance numbers to share.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionNot a problem with the library, but a question regarding usage.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions