-
Notifications
You must be signed in to change notification settings - Fork 7
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
defstruct macro #12
Comments
Hey, thanks for working on this, I'm super glad that you're working towards contributing more! I think your list of features expected is good, although I would add the caveat that I would prefer that only map-interfaced objects be used in the serialization and deserialization of structs. Doing it this way will remove the need to have dynamic dispatch to determine which function to use, instead a single serialize and deserialize function can be defined, and used as the implementations of the multimethods. This would also allow for higher performance usage because it leaves an option up to the user to define a record type which gives fast access to the fields which can reduce the cost of serialization. For deserialization I think it would be a good idea to think some about what the interface could look like to allow deserialization both as a map and as a record based on arguments they pass to the macro. I think that a dedicated class is unlikely to provide a large performance benefit over a record as long as field access is being used rather than the ILookup methods, but using ILookup would allow both maps and record types to be serialized, which I think is likely worth it. To serialize/deserialize in as performant a way as possible I think the baseline of calling the read/write primitive functions with offsets to start. I think there's some room to explore how to deal with struct and other types. Using the deserialize-from and serialize-into functions is an acceptable fallback. I suspect that the function could also be created with an inline definition as well. It might be a good idea to try using a registry of vars that do composite object serdes, mapping from the type name keyword to the vars with the serde functions in them/the symbols that name them. If we go the registry route which atm I think is a good direction to explore that would open up a way to make sure that other macros for composite types can have easy access to functions which do performant serdes without needing the different macros to directly coordinate with each other. I'm not currently in favor of trying to have native-backed memory with clojure interfaces just because it's hard to provide an immutable interface, and I think that if you are working in a situation where the serde costs dominate the time spent in clojure-land between native calls it's likely advisable to just work directly with the read/write primitive functions. I think it's valuable to make that usecase easier to work with (e.g. by adding an offset-of function that allows slicing to subfields of a segment-backed struct value), but I don't know if the defstruct macro is the best place to try to cater to that particular need. I'm also curious what you mean about size-of only working on defalias'd types? The multimethod that powers size-of is c-layout, which has implementations provided for all primitive-backed types and is intended to be implemented by the user if they are making a composite type. Anyway, thanks so much for working on this! I look forward to talking through this some more and hearing back! |
Thanks for the feedback! I didn't expect the vector serialization to a point of contention, so I want to provide some rationale on why i still think it might be valuable to have positional types like vectors supported for defstruct:
I've been using raylib as a case study, since it's a one header library. But here too one can find things like vector types or matrices defined as structs. And a canonical translation of those types to coffi would result in something like this: (coffi.mem/defalias
:raylib/Matrix
[:coffi.mem/struct
[[:m0 :coffi.mem/float]
[:m4 :coffi.mem/float]
[:m8 :coffi.mem/float]
[:m12 :coffi.mem/float]
[:m1 :coffi.mem/float]
[:m5 :coffi.mem/float]
[:m9 :coffi.mem/float]
[:m13 :coffi.mem/float]
[:m2 :coffi.mem/float]
[:m6 :coffi.mem/float]
[:m10 :coffi.mem/float]
[:m14 :coffi.mem/float]
[:m3 :coffi.mem/float]
[:m7 :coffi.mem/float]
[:m11 :coffi.mem/float]
[:m15 :coffi.mem/float]]]) which i really wouldn't want to treat like a map on the clojure side. You could make the argument this maybe shouldn't be a "struct" in the first place, but even here, the order of member names communicates row-major vs column-major layout, so I wouldn't want to lose that information either. So a "tuple" type i would unsatisfactory in this case too. i don't think making that strict distinction would then cover all use cases. As another example, for types like I'm not hard set on this, but i wanted to point out why i think there are cases where this flexibility might be desirable (and a pain to implement with ok performance over and over and then also maintain it.) I agree that defrecord is probably a good place to start and then see if any improvements can be made. I don't think I fully follow with the registry idea. In particular how would creating those vars differ from a protocol or multimethod? Regarding defalias: As you said i could have implemented c-layout, but generating defalias was much quicker and less error prone for the start and I also didn't have to generate everything else at once. But yeah, i'm not planning on keeping it around. Edit: |
Those are all good points! My response is gonna be big so I'll use some headings to keep it organized. Matrix/Vector TypesSo I agree that types like raylib's I think the only real difference in opinion here is where "in the stack" those niceties belong. Personally I'm of the mind that Matrix and Vector are both instances of what I'd consider a special case, where data is neither strictly positional nor strictly map-like. I think there's something to be gained from trying to figure out what is common between those types and others which also have this sort of structure and to try to make something reusable that will be appropriate for types like these. What I'm mostly concerned about here is that I think that Personally, for the Matrix type I would expect an interface using Maybe all this means though is that there needs to be a place in the macro to provide the user with some code to postprocess the data after it's deserialized into a map/record? Then with the matrix you could put it into nested vectors, and with the vector you could construct a new value which implements Existing Struct SerdesThe other thing that we'd be getting into at this stage though if we opened the defstruct macro up to having alternate representations like vectors etc. and part of why I was originally just wanting to stick to maps is that it would also require that the serdes defined for the Right now EDIT: I want to clarify, I'm OK with adding more ways to serde objects, what I was getting at with the past two sections was that I want to think carefully about those new ways, and think about whether they should be under the same name or a new name. I want other creative ways to serde things, it's just a question of where to put it. RegistryThe difference between the registry idea and a multimethod/protocol is just when dispatch happens. If you have a registry available at the top level that maps from type names to the symbols you use to serde that type, then it means you can generate code which includes that symbol in the resulting code, and so the polymorphic dispatch is happening at macroexpansion time. This is a more generic version of what I've been doing with the primitive types in the insn codegen stuff, where I'm looking up the primitive typename and then using that to find the specific asm instructions needed to be called to coerce to the appropriate object type. When doing a multimethod or protocol though, it's doing runtime dispatch between all the available methods, with protocols ofc being much faster than multimethods. This optimization probably won't make a huge difference to most code, but in hot loops it could make a big difference. defalias/c-layoutYeah, I have no issue with how you were implementing it before, I was just puzzled at the statement you made, since the canonical way to define new types without defalias is to either define I see what you meant now though, you were playing around with |
Hey @rutenkolk, I just wanted to check in to see how things have been going and whether you've been able to work on this at all? I'm getting some more time that I might be able to use to work on coffi and I was wondering if you had made what you have so far public anywhere. |
oops, yeah, i'll get to creating a proper fork. i've been hacking on it on and off in a private repo, i'll need a few more days but i'll get to it |
if someone wants to see what i'm doing, i've made progress today. i'll open the PR probably tomorrow. i just realized i coded myself into a corner and arrays are defunctional, so i have to rip that open again, even before a draft PR |
As per #9 (comment)_ i found myself effectively working on a defstruct macro.
It's probably best to not resurrect the old pull request, so i wanted to move the discussion here.
I want to gather some feedback before actually making a new pull request.
Features / Properties I would expect of
defstruct
:defalias
open questions on my end:
current state of my prototype:
mem/sizeof
only works correctly withdefalias
-ed types.example of currently generated code:
There is definitely more work i want to put into this, but i'm interested in feedback on the trajectory on this.
The text was updated successfully, but these errors were encountered: