Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upChange API to use string+len pairs instead of null terminated strings #81
Conversation
#ifdef NK_INCLUDE_STANDARD_VARARGS | ||
NK_LIB int nk_strfmt(char *buf, int buf_size, const char *fmt, va_list args); | ||
NK_LIB int nk_strfmt(char *buf, int buf_size, struct nk_slice fmt, va_list args); |
Hejsil
Mar 11, 2020
Author
Contributor
Ops, this change is a mistake
Ops, this change is a mistake
Here's an idea for a potential ergonomic improvement for c++ users (which I imagine is most people?) Eg: struct nk_slice
{
const char *ptr;
nk_size len;
#ifdef __cplusplus
nk_slice(std::string_view view) : ptr(view.data()), len(view.length) {}
#endif
}; If you're not comfortable using c++17, it could be an std::string& either. |
@wheybags if the proposal is so much dependent on C++ version, shouldn't we rather document it instead of providing it by default in the code? Or maybe implement just a tiny subset which really overlaps between all C++ versions? |
That is why I suggested using an std::string reference instead, that would work on all versions. struct nk_slice
{
const char *ptr;
nk_size len;
#ifdef __cplusplus
# if __cplusplus >= 201703L // if we have c++17, use string view
nk_slice(std::string_view view) : ptr(view.data()), len(view.length()) {}
# else // otherwise fall back to a string ref
nk_slice(std::string& str) : ptr(str.c_str()), len(str.length()) {}
# endif
#endif
}; |
@wheybags hm, I've heard |
I work on a commercial game, and we use the STL heavily. |
Here is my personal take. Nuklear is a C library, not a C++ library. I does the bare minimum to ensure that the library can compile under C++ but nothing more and I think it should stay this way.
There are also a none trivial problem introduced by adding a constructor. An initializer list can no longer be used to initialize this struct: struct nk_slice {
const char *ptr;
int len;
nk_slice(const char *ptr) : ptr(ptr), len(0) {}
};
int main() {
struct nk_slice s = { "t", 1 };
}
So now, our valid C code does not compile under a C++ compiler. |
Well, maybe things like this should live in a separate header. In general, it would be nice to see more ergonomic c++ usage. A supplementary nuklear.hpp could do that. |
Also, c++ have it easy compared to other languages when it comes to simple usage. #include <cstddef>
struct nk_slice {
const char *ptr;
size_t len;
};
struct nk_slice operator "" _nk(const char *ptr, size_t len) {
return {ptr, len};
}
int main() {
struct nk_slice s = "test"_nk;
} |
Well, this sounds like bindings to me, which people already maintain in various languages |
Anyways, I don't think this is to relevant for this specific PR and is something that could be added later anyways. This change would already make the library a little more ergonomic for c++ usage. |
I'm not sure I'm understanding your last two Notes bullet points. In the changes I didn't see any nk_*_label functions changed/removed? Also it looks like all the functions that changed were internal functions, not something I'd call directly from my application GUI code. I hope that's true because I think it'd be sad if C programmers using a C library couldn't do what comes easily and naturally in C, ie pass string literals directly as const char*. Having to do something like this:
would be bad enough. That would be the most efficient way but having to manually count would be annoying and introduce many off-by-1 errors (probably quickly obvious and fixable but still). Compound literals (if you were willing to use them despite the C/C++ issues) would cut a line but leave the counting problem. On the other hand, encouraging C programmers to have dozens or hundreds of little heap allocations for constant (and even update per frame type) strings hurts my soul and is longer.
So, if at all possible, I say keep the interface that let us pass string literals and let the language bindings wrap inner functions that take an nk_slice or equivalent. That's just my 2 cents for what it's worth. |
This PR removes the -NK_API void nk_text(struct nk_context*, const char*, int, nk_flags);
-NK_API void nk_text_colored(struct nk_context*, const char*, int, nk_flags, struct nk_color);
-NK_API void nk_text_wrap(struct nk_context*, const char*, int);
-NK_API void nk_text_wrap_colored(struct nk_context*, const char*, int, struct nk_color);
-NK_API void nk_label(struct nk_context*, const char*, nk_flags align);
-NK_API void nk_label_colored(struct nk_context*, const char*, nk_flags align, struct nk_color);
-NK_API void nk_label_wrap(struct nk_context*, const char*);
-NK_API void nk_label_colored_wrap(struct nk_context*, const char*, struct nk_color);
+NK_API void nk_label(struct nk_context*, struct nk_slice, nk_flags align);
+NK_API void nk_label_colored(struct nk_context*, struct nk_slice, nk_flags align, struct nk_color);
+NK_API void nk_label_wrap(struct nk_context*, struct nk_slice);
+NK_API void nk_label_colored_wrap(struct nk_context*, struct nk_slice, struct nk_color);
Alright, this hits a lot of points. If you are a user of the API, most of the times your code will be updated like this: -nk_label(ctx, "test", NK_TEXT_LEFT);
+nk_label(ctx, nk_slice("test"), NK_TEXT_LEFT); This is not really that bad for a C programmer. It looks pretty clean. Now, you mention a point about efficiency:
Yes, this would be the most efficient way. However let me just point out that the old API called Now, I don't recommend people counting the length of the string and manually inserting it in their code. The best thing you can do in C is to have a macro for this (there are some footguns with this approach, but it is efficient). #define NK_SLICE(str) (struct nk_slice){str, sizeof(str)-1}
nk_label(ctx, NK_SLICE(s), NK_TEXT_LEFT);
And I agree. We should avoid allocations. But with the new API you can more easily do this. Imagine something like this: char *user_input = ...;
// We can now slice the string, without writing '\0' to it or duplicating it.
struct nk_slice sliced = { user_input + 3, 3 };
nk_label(ctx, sliced, NK_TEXT_LEFT); There was no way to do this for some functions with the old API (yes,
This change is is trying to streamline the API, making it more efficient and reduce the surface area. I can flip this question around. Imagine Nuklear was written like this in the first place, and someone wanted to add wrappers to Nuklear that allowed users to pass null terminated strings. I would then ask, should we also have wrappers for |
Ah, I didn't expand the diff on nuklear.h because I was thinking it was the generated nuklear.h so the changes would be the same as the other files. Forgot that there was a src/nuklear.h as well. My bad.
Yeah, I was aware of that. Again this all stems from my not seeing the majority of the changes. Though I don't consider strlen a significant overhead in any case, especially for strings that are usually dozens or at most 100's of bytes. I was comparing to the performance of the added heap operations suggested below which are much slower.
This is probably the best alternative or at least tied with calling nk_slice(), but as you said, there are cases where str isn't an array so sizeof won't work and we have the compound literal I discussed in another paragraph.
I feel like this is a rare use case but I can appreciate shrinking the API while gaining functionality
I get your point, though isn't it UTF-8 by default already and it works as long as the nuklear backend (and font) you're using supports it? I feel that's pretty universal, probably the most popular, and when people are using other rarer character encodings, they're usually having to convert one way or another between interfaces/libraries. Lastly, I will admit that while I'm kind of unique in my programming experience and preferences, I've never really had problems with null-terminated strings. Like everyone else, I've written my own wrapper/dynamic string type when I needed more power, but in and of themselves, I don't see plain old C-strings as particularly error prone. |
Didn't have time to take a look at this rather bigger PR, but I want to say thanks for this very good discussion. Feel free to discuss it further as this will be quite a big thing for Nuklear in general. Judging based on the discussion so far, I really like the implementation approach. Uneducated suggestion: some of the points raised by @rswinkle could be explained in comments in a more direct way (basically formulated as "answers" to @rswinkle's questions) instead of just in this discussion. @Hejsil if you feel we should merge the PR any time soon (days/weeks), please raise your hand as I have currently a lot of other high-priority work to do and would need to reschedule things. And of course, feel free to "recruit" new reviewers to speed things up |
So having comments explaining why we have /* `nk_slice` is the data structure Nuklear uses to pass around strings (in favor of `const char*`).
* This struct carries around both the pointer to bytes and the number of bytes pointed to. Using
* `nk_slice` over a null terminated `const char*` allows Nuklear to avoid calling `strlen`, makes
* the Nuklear API safer, and allows bindings from other languages to be more efficient when
* these languages don't use null terminated strings. `nk_slice` also allows the user to slice up
* a string into multiple parts without allocation, allowing C users to be more efficient as well.
* To get an `nk_slice` from a null terminated string, use the `nk_slicez` function.
*/
struct nk_slice { const char *ptr; nk_size len; };
This will probably take a while (weeks maybe idk), a there is a lot that needs to be changed. It's mostly just a straight forward refactor, so it's just about taking time to getting it done. |
Just put in the last work required to get the entire library compiling. Haven't tested anything yet, but I feel pretty confident about this PR now that I have the entire picture of how the library looks now. Next step is to get all demoes and examples compiling! |
e0d96bf
to
12d3a57
I now have all examples and demos that I can compile on my linux machine working. The primary code in TODO:
|
Once I feel I'm done, I'll rebase on master, bump the major version number and squash most commits together |
If it's just about compilation (not "visual" quality&functionality), then GitHub CI (or other CI of your choice - GitHub CI is though extremely fast - we're talking seconds instead of minutes as with e.g. Travis CI) might help. I didn't have time for that, but if you feel it's a good thing, then we might adopt something similar likehttps://github.com/vlang/v/blob/master/.github/workflows/ci.yml . |
@dumblob Aah, ofc. Idk why I didn't think of using CI for this. I've had good experience with Github CI, so I'll try that out tomorrow. |
Done! All demoes have been compiled and tested. Commits have been squashed. Branch has been rebased on master. I'll take one more look over the changes to make sure I did everything right. After that I'll bump the version and this should be ready for merge. |
0e4b77b
to
ae431f9
Done! |
Just my 2 cents: This really is a big API change. |
@ronaaron @Michael-Kelley @rswinkle Also we (the Immediate-Mode-UI organization) are looking for more members that would be interested in reviewing PR for Nuklear. @dumblob seem to be busy, so having one more person would be very helpful. Any interest here? |
Thanks for pinging me. I am actually very happy about the idea behind this, but I don't have time at the moment to review it. If it's still 'on the ropes' in a week or so, I'll dive in. |
I might be able to look over it next week but my pull request, while a decent size, didn't actually touch Nuklear proper. It was/is just a big demo. The bug I fixed last year was actually really trivial and just involved finding the regression and working around it. All this to say that I'm not sure how useful my reviewing the changes would be but I'll let you know if I do. |
@rswinkle I don't think any real internals changed in this PR. It is mostly changes an API change, with all the code changed to compile under this new API. I mostly need a look over to spot if I made any mistakes converting Anyways, it is up to you if you feel you can contribute a meaningful review |
Wow, that's a lot of code/changes. I mostly ignored/skimmed the changes in the demos/backends etc. and tried to focus on the internals. I tried to check all the nk_substr calls more closely but I may have missed a few in the larger diffs since they were folded when I did my first page search. In general it looks good, nothing obviously wrong jumped out at me (but I did review nuklear_util.c last and my eyes/brain were pretty glazed by that point. I should probably go over it again when I'm fresh). As an aside, it's weird to me to think of passing things that don't fit in a single register by value. I know an nk_slice would be 16 bytes on a 64-bit machine but I've never thought about, tested, or looked up how the calling conventions handle something like that. Would it split it across 2 registers or drop to the stack even if it hasn't used up the 6 registers designated for parameters? Probably the former in a case like this but if it was large enough to overflow the remaining registers that would be awkward to have it half in registers and half in stack. Meh, I know it's irrelevant here. |
} else hash = nk_murmur_hash(name, (int)nk_strlen(name), 42); | ||
if (name.len > 0 && name.ptr[0] == '#') { | ||
hash = nk_murmur_hash(name, win->property.seq++); | ||
name = nk_substr(name, 1, name.len); /* special number hash */ |
rswinkle
Jun 15, 2020
Contributor
I know this is overkill/premature optimization but I can't help wanting to replace that with
name.ptr++; name.len--;
since we already know len > 0 etc. I'd be tempted to make macros for cases like this.
I know this is overkill/premature optimization but I can't help wanting to replace that with
name.ptr++; name.len--;
since we already know len > 0 etc. I'd be tempted to make macros for cases like this.
Hejsil
Jun 15, 2020
Author
Contributor
One thing we could consider is to just have nk_substr
as a static
function in the header only. Since it is really small, it shouldn't bloat users code and making it static
allows the optimizer to inline it if it makes sense.
One thing we could consider is to just have nk_substr
as a static
function in the header only. Since it is really small, it shouldn't bloat users code and making it static
allows the optimizer to inline it if it makes sense.
rswinkle
Jun 16, 2020
Contributor
static? Don't you mean inline or macro? The static keyword makes a function local to a TU ... I don't think I've ever seen it in a header. googles
Dang inline is even more complicated in C than i realized. I was only aware of the inline definition in the header, extern inline declaration in a C file version. Didn't know about static inline.
For those who were unaware like I was, this link sums up the various versions and (in)compatibilities.
Though, from that link, it seems there's a minor problem. Nuklear proper is C89, (not GNU89?) which has no defined semantics. It's probably not a big deal since pretty much no one is using it from strict C89 though.
I do think it's confusing to refer to "making substr static" when static by itself doesn't make any sense in this context. Saying inline or static inline would be more accurate and clear imo.
static? Don't you mean inline or macro? The static keyword makes a function local to a TU ... I don't think I've ever seen it in a header. googles
Dang inline is even more complicated in C than i realized. I was only aware of the inline definition in the header, extern inline declaration in a C file version. Didn't know about static inline.
For those who were unaware like I was, this link sums up the various versions and (in)compatibilities.
Though, from that link, it seems there's a minor problem. Nuklear proper is C89, (not GNU89?) which has no defined semantics. It's probably not a big deal since pretty much no one is using it from strict C89 though.
I do think it's confusing to refer to "making substr static" when static by itself doesn't make any sense in this context. Saying inline or static inline would be more accurate and clear imo.
struct nk_rect src_rect; | ||
struct nk_rect dst_rect; | ||
float char_width = 0; | ||
if (unicode == NK_UTF_INVALID) break; | ||
|
||
// query currently drawn glyph information | ||
next_glyph_len = nk_utf_decode(text + text_len + glyph_len, &next, (int)len - text_len); | ||
next_glyph_len = nk_utf_decode(nk_substr(text, text_len + glyph_len, text.len), &next); |
rswinkle
Jun 15, 2020
Contributor
I don't quite grok what's going on here (even after a cursory glance at nk_utf_decode) but they look different to me.
The old decode would get
len-text_len as c_len
while the new version gets
text.len - (text_len+glyph_len) as str.len aka "c_len"
Am I missing something or is the new version giving a shorter input string/slice? Was the original wrong?
I don't quite grok what's going on here (even after a cursory glance at nk_utf_decode) but they look different to me.
The old decode would get
len-text_len as c_len
while the new version gets
text.len - (text_len+glyph_len) as str.len aka "c_len"
Am I missing something or is the new version giving a shorter input string/slice? Was the original wrong?
Hejsil
Jun 16, 2020
Author
Contributor
Good catch. I don't quite remember why the code ended up like this, so I'll need to look this over again.
Good catch. I don't quite remember why the code ended up like this, so I'll need to look this over again.
Hejsil
Jun 27, 2020
Author
Contributor
I remember why I did this now that I look at it. I was looking over the code and was like "nk_utf_decode only decodes one glyph. I'm pretty sure we don't need to shorten the length of the string here. This will make the code simpler". I'm pretty sure I'm correct in making this simplification.
I remember why I did this now that I look at it. I was looking over the code and was like "nk_utf_decode only decodes one glyph. I'm pretty sure we don't need to shorten the length of the string here. This will make the code simpler". I'm pretty sure I'm correct in making this simplification.
struct nk_rect src_rect; | ||
struct nk_rect dst_rect; | ||
float char_width = 0; | ||
if (unicode == NK_UTF_INVALID) break; | ||
|
||
/* query currently drawn glyph information */ | ||
next_glyph_len = nk_utf_decode(text + text_len + glyph_len, &next, (int)len - text_len); | ||
next_glyph_len = nk_utf_decode(nk_substr(text, text_len + glyph_len, text.len), &next); | ||
nk_rawfb_font_query_font_glyph(font->userdata, font_height, &g, unicode, |
rswinkle
Jun 15, 2020
Contributor
Same here?
Same here?
if (!hex.ptr || !hex.len) | ||
return col; | ||
if (hex.ptr[0] == '#') | ||
hex = nk_substr(hex, 1, hex.len); |
rswinkle
Jun 15, 2020
Contributor
hex.ptr++, hex.len--? Maybe it happens enough to justify a macro? still probably not worth it though.
hex.ptr++, hex.len--? Maybe it happens enough to justify a macro? still probably not worth it though.
Hejsil
Jun 27, 2020
Author
Contributor
Hmm. I'll leave these small optimizations for future PR for when people have found that the current behavior is actually inefficient.
Hmm. I'll leave these small optimizations for future PR for when people have found that the current behavior is actually inefficient.
if (!hex.ptr || !hex.len) | ||
return col; | ||
if (hex.ptr[0] == '#') | ||
hex = nk_substr(hex, 1, hex.len); |
rswinkle
Jun 15, 2020
Contributor
again.
again.
I'll chime in despite not making my hands dirty with the code yet (the following caught my eye when skimming over my inbox).
Due to the enormously wide range of targets Nuklear tries to support, this indeed seems irrelevant - just look at those ubiquitous ARMs with their totally weird mixture of parameter passing - the strategy differs a lot and is too unstable to do any generic optimization (which args - depends on their type - in which order... without any logic - you probably can end up with first arg on stack, second register, next 10 args stack, then 3 args registres, etc.). I'd leave all this completely up to the compiler. Besides having args in registers is no win in general on modern CPUs (this holds also for embedded ones) because of extreme interleaving of instructions and huge caches. So args in registers can easily become slower than if on stack in case one tries to "optimize". And yes, making |
@dumblob I know, modern architectures are crazy with caches, out-of-order execution, speculation (for better or worse) and long pipelines. I'm coming from the perspective of having done a decent number of small to medium assembly programs in the last few years in MIPS, RISC-V and some x86_64. I know there's not a clear performance case for small structures but from an assembly programmer's convenience/readability case, registers beat the stack imo. I know people loved the doubling of registers from 32 to 64 bit that let them change the calling conventions for x86 (and spawned the x32 ABI as well). Combine that with learning a long time ago to avoid passing "large" structures by value to avoid copy overhead (which for me became almost never even for smaller structs) and that's why it seems weird to me. On the subject of ARM, I don't have any practical experience but from what I've seen of it, it's closer to the complexity of x86 than MIPS/RISC-V even though it's technically RISC. And like you said it's become so varied and has so many versions over the last decade or 2 I just haven't had the incentive to learn it. I hope RISC-V grows and becomes more popular. The openness is cool* but it's just so nice as an assembly programmer (and I imagine for a compiler target as well). Sorry to go on a tangent ITT.
|
If someone's interested, here is one prominent example how stack & heap (but using more of out-of-order, speculative processing, ...) beat registers (with less out-of-order, less speculative processing, ...): Hoare’s rebuttal and bubble sort’s comeback |
Hmm. I don't see a way in Nuklear to define a static function in the header. There is only |
Also, thanks for the review @rswinkle. I've explained my reasoning for changing the behavior of the code in the two demoes. I've also judge that it's not really worth it to do all these small optimizations until we actually have some numbers as to whether they would actually make any difference. |
3c1b8b7
to
7388d83
@dumblob Have any thoughts on what we need done before this can be merge? |
Implements this issue.
This pull request doesn't actually implement this. It only contains changes to the header files. This is my proposed new API for strings in the Nuklear library. I would love to hear some feedback on this.
Notes
nk_slice
(nk_str
andnk_text
was taken). This is the string+len pair.nk_slice
from aconst char *
by callingnk_slice("str")
.nk_label
cannot really be changed without throwing away theNK_PRINTF_VARARG_FUNC
attribute. I've therefore chosen to leave these functions as is.nk_*_label
andnk_*_text
variant of varies functions.nk_*_label
is the naming convention that was kept, but these functions now take annk_slice
.Benifits
Cons