-
-
Notifications
You must be signed in to change notification settings - Fork 198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow #[cfg] to be used with #[godot_api] - Part 1: inherent impls #443
Allow #[cfg] to be used with #[godot_api] - Part 1: inherent impls #443
Conversation
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-443 |
b0acd87
to
b1a9a14
Compare
godot-macros/src/class/godot_api.rs
Outdated
let mut param_types: Vec<TyExpr> = Vec::new(); | ||
let mut param_names: Vec<String> = Vec::new(); | ||
|
||
for param in signature.params.inner { | ||
for param in &signature.params.inner { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to add here that the usage of borrowing here was mostly to satisfy the borrow checker, as extract_cfg_attrs
returns a reference to the given attributes, so they shouldn't die with signal
in this loop if we want to use the cfg attrs later. We could, of course, just make util::extract_cfg_attrs
return Vec<Attribute>
by cloning them (that just didn't seem necessary, in principle). Feel free to decide the best approach here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fine, but you could maybe use .iter()
for explicitness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay! Changed it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for addressing this and the accompanying discussion! 💪
If in the future we wanted to extend this to other attributes besides #[cfg]
, what would be necessary?
This might also create minor conflicts with #421; in case that is merged first, this would need rebase/merge.
// Since we're analyzing a field, we don't have access to the function's | ||
// attributes. We have to assume the function exists and has the name the user | ||
// gave us. | ||
// Ideally, we'd be able to place #[cfg_attr] on #[var(get)] and #[var(set)] | ||
// separately, but that is not currently supported. | ||
attributes: Vec::new(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are specifically non-gdext attributes, i.e. excluding #[func]
etc? If yes, maybe name it third_party_attributes
and mention that in the comment briefly (could also be at field declaration rather than initialization).
Btw, lines can be 120+ characters wide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright. I used external_attributes
as that felt less verbose, does that sound OK? (If not, that's totally fine, I can use third_party_attributes
instead.)
I've changed the documentation comments a bit too.
godot-macros/src/class/godot_api.rs
Outdated
let mut param_types: Vec<TyExpr> = Vec::new(); | ||
let mut param_names: Vec<String> = Vec::new(); | ||
|
||
for param in signature.params.inner { | ||
for param in &signature.params.inner { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fine, but you could maybe use .iter()
for explicitness.
Could be, but this is mostly created in one place at the moment? So it might not be that critical.
|
b1a9a14
to
3dc7d32
Compare
That's an important question. In general, you'll have to make sure the attribute you're forwarding to the FFI glue works with any statement, not just function signatures (for semi-obvious reasons: we're not creating a function in the FFI glue). If that's satisfied, and you're sure it'd be worth it to apply to the FFI glue, then you can just modify what happens at the execution sites of Note: the above answer wouldn't apply fully to the other PR (#444) - while the procedure would be similar for that other PR, you'd also have to notice that I use a
In principle, I don't see any conflicts which can arise from #421 with this PR at its current state - but future additions to |
3dc7d32
to
a88d02f
Compare
Yeah I guess it's not that bad, just something to keep in mind in the future - might be useful to know in case this code is eventually modified further later.
Thanks! Over the next few days, I will try to use this to make a more explicit runtime test. (Same applies for the other PR.) |
a88d02f
to
e06e67b
Compare
Okay, added some proper integration tests! Feel free to suggest changes or additions in that regard. With that, I believe most things in this PR should be OK now. Feel free to add further reviews as needed. 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Looks mostly good, some comment/doc feedback.
("A", HasConstants::A), | ||
("B", HasConstants::B as i64), | ||
("C", HasConstants::C as i64), | ||
("D", HasConstants::D as i64), | ||
("CFG_REMOVES_CONSTANT", HasConstants::CFG_REMOVES_CONSTANT), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name is a bit confusing, this looks like it tests for presence of a constant that was deliberately removed.
Maybe name them ONLY_ACTIVE_CFG_CONSTANT
or so. And would be good to add a comment near the definition that the 3 are mutually exclusive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name is a bit confusing, this looks like it tests for presence of a constant that was deliberately removed.
True!
Maybe name them ONLY_ACTIVE_CFG_CONSTANT or so. And would be good to add a comment near the definition that the 3 are mutually exclusive.
Okay. I chose CFG_REMOVES_DUPLICATE_CONSTANT_DEF
as that felt a bit more appropriate, but I can change it to ONLY_ACTIVE_CFG_CONSTANT
if that'd be better.
#[cfg(any())] | ||
#[constant] | ||
const CFG_REMOVES_CONSTANT_FFI_GLUE: bool = true; | ||
|
||
#[constant] | ||
#[cfg(any())] | ||
const CFG_REMOVES_CONSTANT_FFI_GLUE: bool = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is different here, compared to above? What does "FFI glue" mean? Note that tests should ideally be written against behavioral spec/constract, not implementation details.
Maybe choose a more expressive name and add a short description.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it's not very clear. Should probably be better now.
#[cfg(any())] | ||
#[func] | ||
fn cfg_removes_function() -> bool { | ||
false | ||
} | ||
|
||
#[func] | ||
#[cfg(any())] | ||
fn cfg_removes_function() -> bool { | ||
false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could also use compile_error!
, no?
This might also apply elsewhere...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missed it here. Thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added compile_error!
to constants as well now.
#[cfg(any())] | ||
#[func] | ||
fn cfg_removes_function_ffi_glue() -> bool { | ||
true | ||
} | ||
|
||
#[func] | ||
#[cfg(any())] | ||
fn cfg_removes_function_ffi_glue() -> bool { | ||
true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here, not exactly clear what "FFI glue" means 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've renamed the test functions now, should probably be clearer
|
||
#[itest] | ||
fn cfg_doesnt_interfere_with_valid_method_impls() { | ||
// if we re-implement this method but the re-implementation is removed, that should keep the non-removed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: sentence should start with capital letter, and could fit on one line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to surpass 120 characters when kept at a single line, is that ok?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, looked shorter 😀 but yes, 120-145 is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok!
let has_method = |name: &str| { | ||
ClassDb::singleton() | ||
.class_has_method_ex(GdSelfReference::class_name().to_string_name(), name.into()) | ||
.no_inheritance(true) | ||
.done() | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The has_constant
, has_method
and has_constant
could probably be moved out to the global scope, with the class itself being a generic parameter. It's quite likely that we may want to use them elsewhere 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
godot-macros/src/class/godot_api.rs
Outdated
/// The signal's function signature. | ||
signature: Function, | ||
|
||
/// The signal's non-gdext attributes (that is, excluding #[signal]). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be understood as "non-gdext attributes except #[signal]
", maybe make it very clear with "The signal's non-gdext attributes (everything except #[signal])".
Maybe it's just me being a native speaker, but better to have it unambiguous 😉
Same above with #[func]
.
e06e67b
to
1b635d5
Compare
1b635d5
to
18a4611
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for the initiative and the repeated improvements! These quality-of-life improvements are what makes working with the library fun 😊
This is my attempt at allowing
#[cfg]
to work with items in#[godot_api]
impl blocks. This PR only covers inherent impls (non-virtual#[godot_api] impl
blocks). As such, this does not close the #399 issue. That's the job of PR #444.This PR implements solution 1 mentioned in #379 (comment). That is, this is a solution specifically for
#[cfg]
, and does not guard against other possible conditional compilation macros. Those should be used on the entireimpl
if possible (so as to run before#[godot_api]
).If solution 2 is ever made viable, it would supersede this PR, but ATM there doesn't seem to be an easy and viable way for it, so this should work in the meantime.
The gist of this PR is that, for every Godot API attribute type (
func
,signal
,constant
), we store the attributes specified by the user, and later filter them specifically looking for#[cfg]
attributes. Matching attributes are directly forwarded to the generated FFI glue corresponding to each func/signal/constant, such that nothing is registered to Godot if the func/signal/constant is conditionally removed from compilation through#[cfg]
(since it would necessarily also remove the FFI glue).Added some tests to
itests
as proposed in #379 (comment), although the tests can't catch everything as, e.g., signals are always removed from the finalimpl
block so they don't error on duplicates (for instance) regardless of this PR.Examples
#[func]
Consider the following modified code from the Dodge the Creeps example:
Before this PR: Does not compile:
After this PR: compiles (with some warnings since the methods it uses are now unused!).
#[signal]
Consider the following modified code from the Dodge the Creeps example:
It will compile with or without this PR since the
hit()
function is actually removed, so its implementation is not referred to by the FFI glue. But we can note that, without this PR, the signal is still registered in Godot:With this PR, the
cfg
attribute is passed through to the FFI registration block, and thus it will disappear with the signal itself, ensuring the signal won't be registered if it is removed from compilation through#[cfg]
.#[constant]
Consider the following added code to the Dodge the Creeps example:
Before this PR: Does not compile:
After this PR: Compiles.
Other details
Commits
I divided this PR in 3 commits: one for
#[func]
, one for#[signal]
and one for#[constant]
. While this organization is nice to work with, the commits can be squashed if needed.#[cfg(any())]
This is a
cfg
call that represents a contradiction / something which is always false (equivalent to the#[cfg(FALSE)]
idea, but more robust). Used in tests to ensure that the annotated member will be unconditionally removed from compilation, in order to verify that this doesn't cause errors.Discussion points
I made this relatively quickly, so this might need some thoughts regarding design and stuff. The main one is: I created not only an
attributes
field atFuncDefinition
(to store attributes specified above the function), but also the (analogous)SignalDefinition
struct in order to store both the signal's function signature but also the attributes it originally used. This is because, currently, thereduce_to_signature
function removes all attributes from theFunction
object it returns:gdext/godot-macros/src/util/mod.rs
Lines 68 to 71 in 88a7934
And all the signatures used with
#[func]
and#[signal]
attributes go through that function. So, in order to reduce the "blast radius" of the PR (as it could be a breaking change to pass around attributes with said signature objects), I simply added an additionalattributes
field forFuncDefinition
, and createdSignalDefinition
to be able to have a similar attribute for signals.(On an unrelated note, I believe it could be wise to apply the newtype pattern to create a
struct Signature(Function)
type to enforce statically that our invariants for signatures uphold, instead of passing rawFunction
objects, returned byreduce_to_signature
, around - but that is a separate discussion, and is probably largely off-topic for this PR.)Note that
#[constant]
didn't have to undergo much change, as their attributes are not cleared.Another approach here, instead of creating new
attributes
fields, would be to just not clear attributes from signatures, but I don't know if that is safe (I took a look around where they are used, but couldn't determine with full certainty yet), mostly because they could be placed somewhere else and it might be bad for attributes to wherever that is too (...or it could also be desired, unless the attribute changes the body of the function). So, feel free to make a decision here.I added a TODO comment which deserves a look; I added a review comment for it below.
Please feel free to suggest how I could add some runtime tests for this (to ensure the methods were actually 'deleted'). If that'd be excessive, feel free to say so as well.