Skip to content

Conversation

@ghost
Copy link

@ghost ghost commented Mar 12, 2021

This sets the foundations for async-await support in godot-rust, based on the original idea in #284. However, although the tests work, this is not a full implementation:

  • Async methods can only be registered manually using build_method. Macro syntax and implementation are out of the scope of this PR.
  • The runtime types aren't registered automatically yet. Users need to manually call register_runtime and terminate_runtime functions in their library lifecycle hooks. Improving this is out of the scope of this PR for now.
  • The crate is currently re-exported as gdnative::asn, instead of the much longer async_yield. The name is open to discussion -- I don't like it very much.
  • Only local spawners are supported, due to issues with thread safety. Users may off-load tasks that don't contain yield-likes to thread pool spawners using something like futures::future::Remote, however.
  • Panics in async methods don't currently behave very well. Their FunctionState-likes simply block forever and any outstanding bridge objects for futures can be leaked.

While the feature is not yet complete, the commit is already pretty big, and I feel that it's in a somewhat usable state. As a result, I'm putting this up as a draft PR to gather some feedback. If you have uses for async-await / "yield" from GDScript, please feel free to try it and tell me what you think!

Registering an async method currently looks like this (excerpt from the tests):

struct ResumeAddFn;

impl AsyncMethod<AsyncMethods> for ResumeAddFn {
    fn spawn_with(&self, spawner: Spawner<'_, AsyncMethods>) {
        spawner.spawn(|ctx, _this, mut args| {
            let a = args.read::<i32>().get().unwrap();
            let obj = args.read::<Ref<Object>>().get().unwrap();
            let name = args.read::<GodotString>().get().unwrap();

            async move {
                let b = ctx.until_resume().await;
                let b = i32::from_variant(&b).unwrap();

                let c = unsafe { obj.assume_safe().call(name, &[]) };
                let c = Ref::<Reference>::from_variant(&c).unwrap();
                let c = unsafe { c.assume_safe() };
                let c = ctx.signal(c, "completed").unwrap().await;
                assert_eq!(1, c.len());
                let c = i32::from_variant(&c[0]).unwrap();

                (a + b + c).to_variant()
            }
        });
    }
}

fn register_methods(builder: &ClassBuilder<AsyncMethods>) {
    builder
        .build_method("resume_add", Async::new(ResumeAddFn))
        .done();
}

Using it is almost like any other GDScript coroutine:

func _async_call(obj):
	var fn_state = obj.resume_add(1, self, "_get_async_number")
	# the "resumable" signal is unique to Rust, since unlike GDScript coroutines,
	# Rust futures aren't guaranteed to be polled up to a yield right after spawning.
	yield(fn_state, "resumable")
	fn_state = fn_state.resume(2)
	# no "resumable" signal when awaiting signals, though
	var result = yield(fn_state, "completed")
	assert(result == 42)

func _get_async_number():
	yield(get_tree().create_timer(0.1), "timeout")
	return 39

@ghost ghost added the feature Adds functionality to the library label Mar 12, 2021
@ghost ghost marked this pull request as ready for review March 22, 2021 11:38
@ghost
Copy link
Author

ghost commented Apr 15, 2021

Merging this since there doesn't seem to be any objections.

bors r+

bors bot added a commit that referenced this pull request Apr 15, 2021
701: Add automatically_derived and allow lints in macros r=toasteater a=toasteater

This should prevent clippy lints from showing up in user code in the future. See:

- https://doc.rust-lang.org/reference/attributes/derive.html
- https://doc.rust-lang.org/rustc/lints/groups.html
- https://github.com/rust-lang/rust-clippy#clippy

709: Minimal async-await foundations r=toasteater a=toasteater

This sets the foundations for async-await support in godot-rust, based on the original idea in #284. However, although the tests work, this is not a full implementation:

- Async methods can only be registered manually using `build_method`. Macro syntax and implementation are out of the scope of this PR.
- The runtime types aren't registered automatically yet. Users need to manually call `register_runtime` and `terminate_runtime` functions in their library lifecycle hooks. Improving this is out of the scope of this PR for now.
- The crate is currently re-exported as `gdnative::asn`, instead of the much longer `async_yield`. The name is open to discussion -- I don't like it very much.
- Only local spawners are supported, due to issues with thread safety. Users may off-load tasks that don't contain `yield`-likes to thread pool spawners using something like `futures::future::Remote`, however.
- Panics in async methods don't currently behave very well. Their `FunctionState`-likes simply block forever and any outstanding bridge objects for futures can be leaked.

- - -

While the feature is not yet complete, the commit is already pretty big, and I feel that it's in a somewhat usable state. As a result, I'm putting this up as a draft PR to gather some feedback. If you have uses for async-await / "yield" from GDScript, please feel free to try it and tell me what you think!

Registering an async method currently looks like this (excerpt from the tests):

```rust

struct ResumeAddFn;

impl AsyncMethod<AsyncMethods> for ResumeAddFn {
    fn spawn_with(&self, spawner: Spawner<'_, AsyncMethods>) {
        spawner.spawn(|ctx, _this, mut args| {
            let a = args.read::<i32>().get().unwrap();
            let obj = args.read::<Ref<Object>>().get().unwrap();
            let name = args.read::<GodotString>().get().unwrap();

            async move {
                let b = ctx.until_resume().await;
                let b = i32::from_variant(&b).unwrap();

                let c = unsafe { obj.assume_safe().call(name, &[]) };
                let c = Ref::<Reference>::from_variant(&c).unwrap();
                let c = unsafe { c.assume_safe() };
                let c = ctx.signal(c, "completed").unwrap().await;
                assert_eq!(1, c.len());
                let c = i32::from_variant(&c[0]).unwrap();

                (a + b + c).to_variant()
            }
        });
    }
}

fn register_methods(builder: &ClassBuilder<AsyncMethods>) {
    builder
        .build_method("resume_add", Async::new(ResumeAddFn))
        .done();
}
```

Using it is almost like any other GDScript coroutine:

```gdscript
func _async_call(obj):
	var fn_state = obj.resume_add(1, self, "_get_async_number")
	# the "resumable" signal is unique to Rust, since unlike GDScript coroutines,
	# Rust futures aren't guaranteed to be polled up to a yield right after spawning.
	yield(fn_state, "resumable")
	fn_state = fn_state.resume(2)
	# no "resumable" signal when awaiting signals, though
	var result = yield(fn_state, "completed")
	assert(result == 42)

func _get_async_number():
	yield(get_tree().create_timer(0.1), "timeout")
	return 39
```

Co-authored-by: toasteater <[email protected]>
@bors
Copy link
Contributor

bors bot commented Apr 15, 2021

Build failed (retrying...):

bors bot added a commit that referenced this pull request Apr 15, 2021
709: Minimal async-await foundations r=toasteater a=toasteater

This sets the foundations for async-await support in godot-rust, based on the original idea in #284. However, although the tests work, this is not a full implementation:

- Async methods can only be registered manually using `build_method`. Macro syntax and implementation are out of the scope of this PR.
- The runtime types aren't registered automatically yet. Users need to manually call `register_runtime` and `terminate_runtime` functions in their library lifecycle hooks. Improving this is out of the scope of this PR for now.
- The crate is currently re-exported as `gdnative::asn`, instead of the much longer `async_yield`. The name is open to discussion -- I don't like it very much.
- Only local spawners are supported, due to issues with thread safety. Users may off-load tasks that don't contain `yield`-likes to thread pool spawners using something like `futures::future::Remote`, however.
- Panics in async methods don't currently behave very well. Their `FunctionState`-likes simply block forever and any outstanding bridge objects for futures can be leaked.

- - -

While the feature is not yet complete, the commit is already pretty big, and I feel that it's in a somewhat usable state. As a result, I'm putting this up as a draft PR to gather some feedback. If you have uses for async-await / "yield" from GDScript, please feel free to try it and tell me what you think!

Registering an async method currently looks like this (excerpt from the tests):

```rust

struct ResumeAddFn;

impl AsyncMethod<AsyncMethods> for ResumeAddFn {
    fn spawn_with(&self, spawner: Spawner<'_, AsyncMethods>) {
        spawner.spawn(|ctx, _this, mut args| {
            let a = args.read::<i32>().get().unwrap();
            let obj = args.read::<Ref<Object>>().get().unwrap();
            let name = args.read::<GodotString>().get().unwrap();

            async move {
                let b = ctx.until_resume().await;
                let b = i32::from_variant(&b).unwrap();

                let c = unsafe { obj.assume_safe().call(name, &[]) };
                let c = Ref::<Reference>::from_variant(&c).unwrap();
                let c = unsafe { c.assume_safe() };
                let c = ctx.signal(c, "completed").unwrap().await;
                assert_eq!(1, c.len());
                let c = i32::from_variant(&c[0]).unwrap();

                (a + b + c).to_variant()
            }
        });
    }
}

fn register_methods(builder: &ClassBuilder<AsyncMethods>) {
    builder
        .build_method("resume_add", Async::new(ResumeAddFn))
        .done();
}
```

Using it is almost like any other GDScript coroutine:

```gdscript
func _async_call(obj):
	var fn_state = obj.resume_add(1, self, "_get_async_number")
	# the "resumable" signal is unique to Rust, since unlike GDScript coroutines,
	# Rust futures aren't guaranteed to be polled up to a yield right after spawning.
	yield(fn_state, "resumable")
	fn_state = fn_state.resume(2)
	# no "resumable" signal when awaiting signals, though
	var result = yield(fn_state, "completed")
	assert(result == 42)

func _get_async_number():
	yield(get_tree().create_timer(0.1), "timeout")
	return 39
```

Co-authored-by: toasteater <[email protected]>
@bors
Copy link
Contributor

bors bot commented Apr 15, 2021

Build failed:

Copy link
Member

@halzy halzy left a comment

Choose a reason for hiding this comment

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

Just a few comments, will spend more time reviewing the PR.

This sets the foundations for async-await support in godot-rust, based on the
original idea in #284. However, although the tests work, this is not a full
implementation:

- Async methods can only be registered manually using `build_method`. Macro
  syntax and implementation are out of the scope of this PR.
- The runtime types aren't registered automatically yet.
  Users need to manually call `register_runtime` and `terminate_runtime`
  functions in their library lifecycle hooks. Improving this is out of the
  scope of this PR for now.
- The crate is currently re-exported as `gdnative::asn`, instead of the much
  longer `async_yield`. The name is open to discussion -- I don't like it very
  much.
- Only local spawners are supported, due to issues with thread safety. Users
  may off-load tasks that don't contain `yield`-likes to thread pool spawners
  using something like `futures::future::Remote`, however.
- Panics in async methods don't currently behave very well. Their
  `FunctionState`-likes simply block forever and any outstanding bridge
  objects for futures can be leaked.
@ghost
Copy link
Author

ghost commented Apr 16, 2021

Updated.

bors try

bors bot added a commit that referenced this pull request Apr 16, 2021
@bors
Copy link
Contributor

bors bot commented Apr 16, 2021

try

Build succeeded:

Copy link
Member

@halzy halzy left a comment

Choose a reason for hiding this comment

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

PR looks great! Just some minor questions.

Comment on lines +71 to +72
let id = pool.next_id;
pool.next_id += 1;
Copy link
Member

Choose a reason for hiding this comment

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

A bit of nitpicking here, this could be impl'd on pool.

if called {
Ok(())
} else {
Err(InitError::new())
Copy link
Member

Choose a reason for hiding this comment

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

When loaded in the Editor and one is iterating on a godot-rust project, the register_runtime() function is called more than once. This causes the InitError to be returned.


/// Sets the global executor for the current thread to a `&'static dyn LocalSpawn`.
pub fn set_executor(sp: &'static dyn LocalSpawn) -> Result<(), SetExecutorError> {
LOCAL_SPAWN.with(|cell| cell.set(sp).map_err(|_| SetExecutorError::new()))
Copy link
Member

Choose a reason for hiding this comment

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

Same issue here, when running in the editor this will be called more than once when iterating.

@ghost ghost added this to the 0.10 milestone Apr 27, 2021
@ghost
Copy link
Author

ghost commented Jun 11, 2021

Seems like the init issues are related to #746?

@ghost ghost added the status: postponed label Jul 5, 2021
@ghost
Copy link
Author

ghost commented Jul 5, 2021

Blocked by #757: the init errors are caused by bugs / new behaviors in Godot 3.3.

It'd imo also be acceptable to require reloadable = false for this early version, if we want to merge this right now, since it isn't intended to be the final interface for most use cases.

@Bromeon
Copy link
Member

Bromeon commented Oct 25, 2021

Superseded by #804.

@Bromeon Bromeon closed this Oct 25, 2021
bors bot added a commit that referenced this pull request Oct 27, 2021
804: Minimal async-await foundations, updated r=Bromeon a=chitoyuu

This is an updated and rebased version of #709, that is no longer blocked on #757. Notable changes outside the original PR include:

- `TerminateInfo` and `InitializeInfo` are moved back into the public API. These types are used as arguments to custom init/terminate callbacks. Custom callbacks are necessary for calling the `gdnative_async` `register` and `terminate` functions. Currently, they are just moved into the top level of `gdnative_core`, which I think makes sense given their usage in top-level callbacks. Please let me know if a better place is available.
- Renamed the top-level re-export from `asn` to `tasks`, since the original author apparently didn't like it.

Supersedes #709

Co-authored-by: Chitose Yuuzaki <[email protected]>
Co-authored-by: toasteater <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Adds functionality to the library status: duplicate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants