-
Notifications
You must be signed in to change notification settings - Fork 632
TypeDesc Rust Bindings #4643
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
base: main
Are you sure you want to change the base?
TypeDesc Rust Bindings #4643
Conversation
I think a Rust binding ideally feels like it isn't a binding but an API of any well-executed crate (pure Rust). If a user is confronted with the oddities (from a Rust perspective) that the shortcomings of the original languange dictate because they're forwarded in the wrapper, you failed on the developer UX part of writing a wrapper. In that sense, the What I did in my binding:
|
Fair points. I think the hard part at the moment is that the Rust API should match the C++ API as much as possible (there's a change that I'm going to revert in my code where I've changed what would be an |
I 100% understand where you're coming from and admire the sense of purity you're striving for. There are even things about TypeDesc I wish I'd done differently in C++. But I think that redesigning every bit of our APIs is not the mission this time around. For stage 1, we just want to wrap what we have in as direct a way that we can while still being acceptable Rust. Stage 2 might be a rethink, with the 20/20 hindsight of experience using it (including the C++ side getting more experience with Rust and bringing good ideas back to the C++ side).
No, of course not. The names of the language's types are what they are. The names of our enum values aren't chosen to be identical to type names in C++ (for example, there are no C++ language types called UINT8 or HALF), and the names we used for Python didn't change (it's still called STRING for our Python APIs, we didn't call it STR to match Python's As for TypeDesc in particular, again I am not necessarily criticizing the theoretically good ideas you have for a (maybe better) abstract design, but I am weighing them against some concrete design values at this time:
|
This is a mistake, IMHO. Apart from deeming the approach wrong, it's also more work meaning more people are needed or stuff will just take longer. I'm urging you to check progress ASWF Rust wrappers have made in the last 2--3 years. More or less none. Who will do this stuff? This bit maybe the pink elephant in the room, even though it's OT here. It was what I was trying to convey in the thread in Slack and may have failed at.
So are the uppercase enum variant names. They are capitalized names of the POD types and used as such throughout the entire ecosystem of crates when a POD type has to be conveyed through an enum variant. That's what I use in
I don't think you understand how uniform the Rust ecosystem is and how much people adhering to one set of common names and naming conventions is part of that. I think adoption has higher value than consistency accross languages. And adopters in Rust land, outside of the tiny, narrow field of VFX, will not be people who care about matching API in Python/C++. Because these are two of the most shunned languages by Rust people. If you ignore that you're just saying you care more about consistency than adoption. What you get if you go down this road is just not what a "Rust binding" is in Rust land from my experience of using such bindings for years now.
I disagree. I think it's moot to write a Rust binding then. See also below re. the For representations it should be "as necessary" and for names it should be "as possible". Which means best practices and offcial guidelines for the target language take precedence.
I think given the time typical OIIO operations take the But unfortunately, that's not what you're saying here.
96bit. The reason is the last part, const I32_MAX: usize = i32::MAX as _;
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
#[repr(i32)]
pub enum ArrayLen {
Specific(Refinement<u32, ClosedInterval<1, I32_MAX>>),
#[default]
Unspecific = -1,
} This is ensures you can only store unsigned values between Those possibilities are exactly the reason you use Rust. If you replicate the C++ API that requires you to take care yourself to not store negative array lengths or make sure you use the right magic number: why make the effort to write a Rust wrapper at all? I.e. to hark back to what you said above that "there is value" in this: I don't see it. Lastly, all this also means I won't be able to contribute to the Rust wrapper, which is a pity. I have very little time to work on the OIIO stuff and my mandate is that the people I work with can use it, without hand-holding and with foot guns removed, where possible. I.e. they're exactly the kind people representative of the biggest group of users of an OIIO wrapper in Rust land, in the future. 😉 |
Maybe I don't understand the implementation of Just picking the arraylen part to focus on, is there a reason why the following won't work (excuse me if my syntax is wrong, but I think you'll know what I mean):
which as far as I understand is bit-for-bit the same as the |
I can comment on the |
As Scott wrote, it depends. In general, wrapping an Consider: use std::mem::size_of;
#[repr(u8)]
enum MyEnum {
Foo,
Bar,
Baz,
}
assert_eq!(size_of::<MyEnum>(), size_of::<Option<MyEnum>>()); Because of that, in the case at hand, the #[repr(C)]
pub struct TypeDescTest {
pub base_type: Option<BaseType>,
pub aggregate: Aggregate,
pub vec_semantics: Option<VecSemantics>,
pub _reserved: u8,
// No ArrayLen.
}
assert_eq!(size_of::<TypeDescTest>(), 4);
Just to clarify again: the size increase does not come from the To answer the question: because adding a Dozens of patterns that are common is Rust will not work with For example: let my_vec = my_type_desc.array_len
.and_then(|array_len| {
// Do something applying to arrays.
})
.ok_or_else(|| {
// Do something else when it's not an array.
})?; That is Rust. Rust may look familiar to C/C++ when you are e beginner but it looks quite a bit different once you start writing idiomatic code. Above is a completely made-up example but just check the methods available on Whenever you have something that can error, you wrap it in a Adding a In the case at hand the size increase comes from the fact that the Rust enums have a hidden discriminant which is the number of the variant (see Scott's comment -- they're essentially tagged To cram it into 32bit, the use refined::{boundable::unsigned::ClosedInterval, Refinement};
use std::mem::size_of;
const U16_MAX: usize = u16::MAX as _;
const I32_MAX: usize = i32::MAX as _;
pub enum ArrayLenSmall {
Specific(Refinement<u16, ClosedInterval<1, U16_MAX>>),
#[default]
Unspecific = -1,
}
pub enum ArrayLen {
Specific(Refinement<u32, ClosedInterval<1, I32_MAX>>),
#[default]
Unspecific = -1,
}
assert_eq!(size_of::<Option<ArrayLenSmall>>, 4);
assert_eq!(size_of::<Option<ArrayLen>>, 8); TL;DR: |
Leaning into using Option, I think that maybe we should just eliminate the basetype NONE/UNDEFINED enum value entirely. When we don't know or don't want to specify a type, we don't want ANY of these fields, so it seems to me that the right way to use Option is that some functions or data might have an I'm on the fence about VecSemantics (which I would like to rename Semantics, since it has long since grown beyond just describing the transformation semantics of a vector). As a general design, I can see the merit of an Option, but also I'm not yet convinced that it it's a bad thing to use an enum value. I dunno. If Scott also agrees with you, then I guess I'm ok it for now and we'll see how it goes. So, to summarize where I'm at on all the issues:
On the wholesale renaming of the BaseType enum tags: hard no from me on that one. They don't match "C++" in their other uses, so they don't need to be converted to match Rust names here. I understand that if Rust was the only/first binding we had, maybe we would have chosen names that matched Rust nomenclature (and in that alternate universe, I would similarly not let the C++ bindings deviate from the established names). On this matter, I put more weight on having uniform nomenclature between C++, Python, Rust, command line utils like oiiotool, and concepts we talk about in the docs, even if it makes any or all of the individual language bindings slightly less familiar. Separate issue: I have sometimes wished that on the C++ side, I had made the data fields private and added access methods. Does idiomatic Rust prefer accessors or direct struct member use by client code? If the former, I sure don't mind doing it here, and it's another way to allow us to make the API a bit more insulated from any back-and-forth about how we represent the data underneath. |
Just going to quickly summarize in my own words with my thoughts.
Also, side note/question that I realize that I forgot to mention. I've dropped fields such as As for your question about getters/setters vs directly accessing the attributes, I've heard arguments for both sides. If I recall, the main negative for the getters/setters is I've heard that it can mess with the borrow checker (take this with a grain of salt, my memory on this is fuzzy at best). Personally, I prefer having all of my attributes private, and having specific getters and setters that control how the user of the API accesses my data. But, there are some cases where it makes sense to have direct access to the attributes, such as with the options struct that @virtualritz mentioned when you have optional arguments for a function. |
Aha, the docs say
So presumably, under the covers, it's using the 0 value to indicate that the option is None? You know what? I'd be ok with that as the solution to the arraylen issue. It satisfies my inclination to keep the underlying representation the same size and layout, even if the language expression is different between Rust and C++.
We never have negative image dimensions. I think it's probably ok to use unsigned for that rather than signed.
Correct, and that's fine. I prefer the names that include the size even on the C++ side. |
For arraylen, on the C++ side, we use -1 to signify "it's an array, but we don't know the length". I'm not 100% sure that this is used in OIIO anywhere, but we certainly use it in OSL, which also uses OIIO's TypeDesc. |
I'm not sure how that will help for The That's precisely why I used |
I would use private If there are no invalid combinations, go with public struct fields. The code setting the The middle way is a builder pattern. Then you need to decide if the |
Sorry, to be specific, I wasn't saying that we should use pub struct ArrayLength(i32);
let array_len = ArrayLength::new(1)?; // This will return a result if the number is less than 1 or 0, whichever we prefer.
let undefined_array_len = ArrayLength::undefined(); // This will return ArrayLength(-1) But honestly, I need to give this more thought. But, at the end of the day, if we make the interfaces like the below, then this should be a bit moot. pub fn new<A: TryInto<ArrayLength>>(base_type: BaseType, agg: Aggregate, semantics: VecSemantics, arraylength: A) -> Result<Self, OiioError> {...} |
You would absolutely do that on the Rust side anyway. Incl. use of It's one of the forever WTFs in C/C++ land, the use of |
Yeah, and that type is what goes into the const I32_MAX: usize = i32::MAX as _;
/// This *is* your type for the array length:
pub type Len = Refinement<u32, ClosedInterval<1, I32_MAX>>;
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
#[repr(i32)]
pub enum ArrayLen {
Specific(Len),
#[default]
Unspecific = -1,
}
Ouch. I think I'm giving up now. Apart from the fact that this should be This is not only about making this feel natural to a Rust developer. While the |
@lgritz I don't understand the rationale here. The users of the API are Rust developers. They know these tags by heart. They do not know the ones on the C++ side. |
There are at least three distinct sets of people I am considering here:
While I admire your aspirations for what we could eventually be for the Rust ecosystem, I think of group 1 as only a minor constituency for the next couple years. I think and hope they will become more important in the future, but today they don't even know we exist. |
I think this is a lost cause for me. I don't know how to say this better: I don't think there is enough Rust experience here, especially understanding of the type system and how big a role unified naming plays in the accessibility of the language's ecosystem, to make these calls. |
Currently blocked by dtolnay/cxx#1519 - It looks like some of the functions are passing the |
3c183ba
to
cb53400
Compare
22b6429
to
233a86b
Compare
Just as a tip: I don't find "merging" to be very useful in a long-lived feature development branch. Personally, I prefer to "git rebase main" periodically to move feature branch commits to be simple, linear, and on top of the current master, rather than end up with a complex tangle of multiple branch-to-branch merges from different points in the past. |
- Remove some test code - Disable Rust building on `ICC` and `ICX` Signed-off-by: Scott Wilson <[email protected]>
…b dirs via environment variables Signed-off-by: Scott Wilson <[email protected]>
… dir Signed-off-by: Scott Wilson <[email protected]>
Description
This is my initial stab at creating an (official?) wrapper for OIIO in Rust. The current goal is to tackle one module/class at a time and get it to 100% (sys crate, API crate, tests, and docs) before moving onto the next.
Tests
Testing is in progress, but the tests should only cover if the wrapper is doing what it is supposed to. If C++ side has an error, then it'll not be considered an error in the wrapper.
Checklist:
need to update the documentation, for example if this is a bug fix that
doesn't change the API.)
(adding new test cases if necessary).
corresponding Python bindings (and if altering ImageBufAlgo functions, also
exposed the new functionality as oiiotool options).
already run clang-format before submitting, I definitely will look at the CI
test that runs clang-format and fix anything that it highlights as being
nonconforming.