-
Notifications
You must be signed in to change notification settings - Fork 235
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
Unable to conditionally enable uniffi::constructor macro #2000
Comments
Yeah, it is infeasible for uniffi to include its own parser + interpreter for cfg expressions such that it could know whether a cfg_attr should apply or not. I've wanted to solve this with a |
I came here to report this bug and I found this issue had already been opened. The conditional works for I expected this to work similarly in both APIs, even with cfg aliases: #[cfg_attr(wasm, wasm_bindgen)]
#[cfg_attr(not(wasm), uniffi::export)]
impl SomeObject {
#[cfg_attr(wasm, wasm_bindgen(constructor))] // works as expected, compiles
#[cfg_attr(not(wasm), uniffi::constructor)] // this issue
pub fn new() {
...
}
} The error string comes from |
A workaround is possible with the cfg_if::cfg_if! {
if #[cfg(not(wasm))] {
#[uniffi::export]
impl MyObject {
#[uniffi::constructor] // the macro is parsed correctly because it's standalone now
pub fn new() {
...
}
}
}
} |
The actual parsing happens at The Maybe a full-blown parser isn't necessary. We could optimistically ignore the first cfg_attr argument and apply the current logic to whatever we find in the second argument. This would be a quick-and-dirty solution for this specific case, but I'll investigate further how |
It seems This is the relevant bit at wasm_bindgen/crates/macro-support/src/parser.rs, line 204: fn find(attrs: &mut Vec<syn::Attribute>) -> Result<BindgenAttrs, Diagnostic> {
let mut ret = BindgenAttrs::default();
loop {
let pos = attrs
.iter()
.enumerate()
.find(|&(_, m)| m.path().segments[0].ident == "wasm_bindgen")
.map(|a| a.0);
... // some validation i.e. checks if the parenthesis closes
}
...
} |
That code you quoted does not look for |
They don't look inside In our case, the parser expects a path with exactly two symbols, the first being "uniffi" and the second "constructor" or "method". We should not validate the total number of symbols in the attribute, but only the presence of "uniffi::{constructor|method}" and ensure they are not duplicated. This would solve the problem for most use cases and keep the existing behavior. |
If you agree with this approach, I can write a PR sometime this week. |
What do you think avout the alternative of a fake cfg? I mentioned it above but I realize I didn't provide an example. What I mean is specifically to make the macros look for |
Looking at the docs, my first impression is that it supports so many variations that we would have to deal with cases we cannot hope to foresee, especially combined with We could explicitly detect |
No, we wouldn't have to support any of that. I'm pretty sure just |
The problem with this syntax is that In my example, Without the alias, this is the actual syntax the compiler expects: #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[cfg_attr(not(target_arch = "wasm32"), uniffi::export)]
impl SomeObject {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] // works as expected, compiles
#[cfg_attr(not(target_arch = "wasm32"), uniffi::constructor)] // this issue
pub fn new() {
...
}
} Users can come up with more granular cases. For example, one might want to support WASM, iOS, and Python bindings but use Pyo3 instead of uniffi for Python. In this case, both examples below would be valid syntax: #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[cfg_attr(not(target_arch = "wasm32"), cfg_attr(target_os = "ios", uniffi::export))]
#[cfg_attr(not(target_arch = "wasm32"), cfg_attr(not(target_os = "ios"), pymethods))]
impl SomeObject {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] // wasm-bindgen
#[cfg_attr(not(target_arch = "wasm32"), cfg_attr(target_os = "ios", uniffi::constructor))] // uniffi
#[cfg_attr(not(target_arch = "wasm32"), cfg_attr(not(target_os = "ios"), new))] // pyo3
pub fn new() {
...
}
}
// OR
#[cfg_attr(not(target_arch = "wasm32"), cfg_attr(feature= "pyo3"), pymethods))]
#[cfg_attr(not(target_arch = "wasm32"), cfg_attr(not(feature = "pyo3"), uniffi::export))] My proposal is that we don't need to care about any of that. We can safely assume that if Parsing and interpreting |
No, any raw identifier in |
I'm sorry if I didn't express my point clearly. It wasn't about it failing. My intention was more about highlighting user's expectations and keeping consistency with similar APIs. We could have a // Is the idea to defensively only allow this version?
#[cfg_attr(uniffi, constructor)]
// Is this also supported? Would we allow, require, or prohibit it?
#[cfg_attr(uniffi, uniffi::constructor)]
// Would it support arbitrarily nested predicates?
#[cfg_attr(foo, cfg_attr(bar, cfg_attr(uniffi, constructor)))]
#[cfg_attr(foo, cfg_attr(bar, cfg_attr(uniffi, uniffi::constructor)))]
// If nesting is supported, wouldn't this be easier to implement?
// (reuses the existing logic, removes ambiguity)
#[cfg_attr(foo, cfg_attr(bar, uniffi::constructor))] We can achieve the last version with minor changes to |
The idea is to only support the first thing. No nesting, probably not even the second version with repeated |
Thanks for clarifying. I think it would be too restrictive and also would increase the current API surface, which would need to be documented. The parser is already looping through all annotations and ignoring everything that does not start with Looping inside each annotation until we find It's just about wrapping the current loop into another loop. I believe users should have total control over the predicate (cfg_attr's 1st argument), because it represents their unique requirements. It's outside |
I think there is some misunderstanding going on here, but I haven't done a great job at digging into what it is exactly. When you say UniFFI should support ¹ assuming that |
You seem to think that uniffi currently special-cases |
Yes, I suggest it always. Semantically, if the user includes If they include This isn't a footgun because we're dealing with pre-compile time. Whatever the user does with their predicates is their power and responsibility, and should not be our concern. It's the compiler's job to tell them they're doing something wrong or inconsistent, not uniffi's. Unless the lib was specifically designed to target the predicate (ex. cfg_alias crate), it is out of scope to care about it.
No; I know it currently does not special-cases
That's not the reason. The reason is our parser expects it to be exactly This wouldn't break any existing codebase, and add support for custom conditions. |
What do you mean by "add support for custom conditions"? They would not be supported, they would just be accepted, but ignored as if they were always true. Your suggestion would be equally powerful to mine - allowing uniffi attributes to be used when the export macro is used conditionally, but not independently have their effects apply conditionally - while syntactically suggesting to users that the first |
Thanks for your insights and patience so far! The first argument is meaningful in the sense of explicitly adding or removing // This is the normal uniffi's use case:
#[uniffi::export]
impl SomeObject {
#[uniffi::constructor]
pub fn new() { ... }
} So // This is the normal `cfg_attr` use case
#[cfg_attr(my_condition = "true", uniffi::export)]
impl SomeObject {
#[cfg_attr(my_condition = "true", uniffi::constructor)]
pub fn new() { ... }
}
// Both conditions will usually match cargo.toml:
// ----------------------------------------------
[target.'cfg(my_condition = "true")'.dependencies]
uniffi = { version = "0.27" } In this use case, all conditions must always match the dependency, and the compiler will enforce it. If the conditions are inconsistent, the program won't compile because it will eventually try to use a module that is out of scope. I can't imagine a use case where someone would need to conditionally mark a function as a constructor. Normally, the goal of In summary, Risks of ignoring cfg_attrIn the current implementation, we already ignore #[uniffi::export]
impl SomeObject {
#[cfg(false_condition)] // uniffi currently ignores it
#[uniffi::constructor]
pub fn new() { ... } // the function won't be compiled, but I assume uniffi still exports its binding,
} // causing a runtime panic if called Ignoring #[uniffi::export]
impl SomeObject {
#[cfg_attr(my_condition = "false", uniffi::constructor)] // false, but uniffi would ignore it ¯\_(ツ)_/¯
pub fn new() { ... } // function will be compiled and recognized as a constructor, no panic at runtime
} Comparing the AlternativesThis is how the actual usage would look like after the change, assuming the normal Alternative 1: ignoring all external APIs (my suggestion)#[cfg_attr(my_condition = "true", uniffi::export)]
#[cfg_attr(my_condition = "false", some::other_lib)]
impl SomeObject {
#[cfg_attr(my_condition = "true", uniffi::constructor)]
#[cfg_attr(my_condition = "false", some::other_lib)]
pub fn new() { ... }
} Alternative 2: fake cfg_attr predicate (your suggestion)#[cfg_attr(my_condition = "true", uniffi::export)]
#[cfg_attr(my_condition = "false", some::other_lib)]
impl SomeObject {
#[cfg_attr(uniffi, constructor)]
#[cfg_attr(my_condition = "false", some::other_lib)]
pub fn new() { ... }
} Similarities
So, while I prefer Alternative 1 for consistency and the other reasons above, I'm willing to compromise. |
Okay, so at least we have the same basic understanding :) I agree that nobody would want to conditionally mark something as a c'tor, but maybe some users would want to conditionally rename a method for certain bindings (which could be approximated with doing it for a specific raget OS, e.g. android), which is a thing with |
Sorry for my late answer, I've got dragged into other priorities last week. You mean conditionally using #[uniffi::export]
impl SomeObject {
#[cfg_attr(target_os = "android", uniffi::constructor(name = "new"))] // renames it for Android
#[cfg_attr(target_os = "ios", uniffi::constructor)] // does not rename it for iOS
pub fn some_static_method() -> Self {
...
}
} This could also be a possible use case for nested predicates, i.e.: #[cfg_attr(wasm, wasm_bindgen)]
#[cfg_attr(not(wasm), uniffi::export)]
impl SomeObject {
#[cfg_attr(wasm, wasm_bindgen(constructor))]
#[cfg_attr(not(wasm), cfg_attr(target_os = "android", uniffi::constructor(name = "new")))]
#[cfg_attr(not(wasm), cfg_attr(target_os = "ios", uniffi::constructor))]
pub fn some_static_method() -> Self {
...
}
} Well, not actually useful (target_os = "..." is never WASM), but users would expect this syntax to be valid. |
Anyway, I cloned the repo and tried to run the tests both locally and on Docker (trunk without changes), but could not get them to work for Kotlin. I implemented my suggested change but because I couldn't run the entire test suite, I didn't submit a PR yet. Can I submit it without the Kotlin tests running locally? My results with and without the changes are exactly the same for all the other platforms. Or should I have the entire suite running first? |
This PR solves mozilla#2000 - Unable to conditionally enable uniffi::constructor macro.
Coming in late to this one, and I'm not sure I understand it all, but I wonder if it would be possible to rewrite each method by convert the Note: In order to support UDL, we already have code to parse an item definition, generate the UniFFI code needed for it but throw away the actual item. So, if UniFFI processed:
...it would generate this additional code:
This would require a some parsing to handle nested |
Maybe we could simplify the parsing, if we told users they had to use our syntax for conditionals:
|
That sounds like a really good idea, if it can work reliably! (haven't thought it through completely, but sounds reasonable) |
Hey guys, sorry for my delay. I have moved to other issues and postponed the fix in my original PR, but #2113 represents exactly my original intention, so I closed my draft PR in favor of it. I'm against introducing new syntax to deal with this problem. This would create the need to build a special parser and document the new behavior. If we keep our API intact and play along with the rest of the ecosystem, there's no need to document it. It'd favor the principle of least astonishment. PS: I left a comment in the PR. Let's continue the discussion there. |
Now that the PR is merged, the public docs are updated, and this is working in production, we can close this issue. Someone arriving here from the link in the docs will assume this is unsolved at first glance. |
I have a simple class that im trying to expose with proc macros like below:
my problem is that i only want uniffi scaffolding if my target is compiling for mobile - not wasm. In the past I have used conditional compilation for this with a lot of success. But when i try the following:
I get the error
associated functions are not currently supported
:For now I'm just putting a
#[cfg(not(target_family = "wasm"))]
on the whole block as a workaround, but i think this could be a bug in one of the macrosThe text was updated successfully, but these errors were encountered: