-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
HTML5 API: Add support for 'input' event #23550
base: main
Are you sure you want to change the base?
HTML5 API: Add support for 'input' event #23550
Conversation
In review please consider one problem: The EmscriptenInputEvent::data field is potentially a very long UTF8 string on the JS side, because the input event could be a paste of a long text. In my implementation this string is truncated to 256 chars, because I was not sure how to handle the JS to wasm handover otherwise. I think the truncation is ok for most use cases of the input event (e.g. form validation where I want to read the DOM value of the event target anyway). However, I am concerned about truncating a UTF8 string, because this could split a code point midway. I am sure there is some experience with this kind of problem in this code base. Any advice? You may want to try the new interactive test case: |
71393fa
to
be16f63
Compare
I think I need some help to interpret the CI result. I don't understand why Edit: Oh, I see, looks |
Those tests are known to be flaky. We can re-run the CI and they should pass. |
system/include/emscripten/html5.h
Outdated
@@ -189,6 +190,15 @@ EMSCRIPTEN_RESULT emscripten_set_focus_callback_on_thread(const char *target __a | |||
EMSCRIPTEN_RESULT emscripten_set_focusin_callback_on_thread(const char *target __attribute__((nonnull)), void *userData, bool useCapture, em_focus_callback_func callback, pthread_t targetThread); | |||
EMSCRIPTEN_RESULT emscripten_set_focusout_callback_on_thread(const char *target __attribute__((nonnull)), void *userData, bool useCapture, em_focus_callback_func callback, pthread_t targetThread); | |||
|
|||
typedef struct EmscriptenInputEvent { | |||
EM_UTF8 data[EM_HTML5_LONG_STRING_LEN_BYTES]; |
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 think we should avoid truncation here and instead consider two possible options:
- Use stack allocated space. i.e.
stringToNewUTF8OnStack()
on the JS side. This would means it would be to up the user to make a copy of the string if they want it to outlive the callback. - Use heap allocation. i.e
stringToNewUTF8
on the JS side. It would then be incumbent upon the receiver to callfree
on the resulting pointer when they are done with it.
I guess (2) seems safer since it avoids a possible stack overflow.
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 for the suggestions. I think (1) is out of question because it is very easy even for an end user to create very large change data
values by Ctrl+A, Ctrl+V or similar. Crashes will happen.
I don't feel too good about (2) either because there will be different target groups of users . Some can completely ignore the provided data
field. E.g. for my current use case which is form validation. I just need to know that something changed and will request the value from the DOM element anyhow. It is waste to perform an allocation here and require the user to take ownership. I can already smell all the memory leaks produced in situations where the data field is ignored. Other users typically only care about a single input char because they deal with keystroke-like stuff.
Maybe we could have independent sets of registration and callback functions:
- Requests input events with full data via
$stringToNewUTF8
: for those who really need all the data and have to pay in terms of performance due to an allocation being made and dealing with ownership semantics - Requests input events without data; still fixed fields (
inputType
, etc.) could be provided: for those who don't need any data and are just waiting for an any-change hook. - Requests input events with truncated data; the data field is partially copied (as currently implemented): for those who deal with keystrokes and other small input data sets (e.g. see use case in Improve support in SDL2 for keyboard input on mobile browsers #21960) which may occur with high rate and the input delta data is still relevant; where performance is key.
I think case 2 is optional, because the target group could use case 3 as well and ignore the provided truncated data. Any thoughts?
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.
How about anther option: Use a dyanmic / heap allocation, but have it live only for the life of the callback.
You could use realloc
on a fixed pointer to avoid allocation except when the region grows.
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.
See SDL_GetKeyName in libsdl.js for an example to using a fixed buffer with realloc
.
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.
Great, realloc
looks perfect, I'll go this route. 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.
Implementation uses realloc
for events on the default thread, now.
For all other threads I am allocating every single time as other event types seem to do. Is there some kind of free()
required? I don't understand the semantics involved here.
test/test_html5_input.c
Outdated
|
||
#ifdef REPORT_RESULT | ||
REPORT_RESULT(testStatus()); | ||
#endif |
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.
Can you just do exit(testStatus())
here and use btest_exit
to run the test? This might require using emscriopten_runtime_keeplive_push()
in main and emscripten_runtime_keeplive_pop()
here.
In fact maybe just inline testStatus
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.
Can you just do
exit(testStatus())
here and use ...
I did the best I can. This works when testStatus() returns 0, then the browser will close and the test suite stops. However, on non 0 testStatus(), the browser does not close and the test suite remains stuck. This is a little inconvenient.
In fact maybe just inline
testStatus
here?
I would like to keep the checks in a function, because the function definition can stay very close to the observed test status variable definitions. In this way it is more obvious the check function needs to be upgraded when tests are amended.
I also added |
Required due to new function emscripten_set_input_callback_on_thread .
4728dca
to
f07bcd7
Compare
rebased to latest master |
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.
var isComposing = e.isComposing; | ||
const dataLengthWithTermination = lengthBytesUTF8(data) + 1; | ||
const eventSize = {{{ C_STRUCTS.EmscriptenInputEvent.__size__ }}} + dataLengthWithTermination; | ||
if(JSEvents.inputEvent == null){ |
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.
Space after keywords like if
.
#endif | ||
|
||
var inputEventHandlerFunc = (e = event) => { | ||
var data = e.data || ''; |
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.
We use 2-space indentation.
const dataLengthWithTermination = lengthBytesUTF8(data) + 1; | ||
const eventSize = {{{ C_STRUCTS.EmscriptenInputEvent.__size__ }}} + dataLengthWithTermination; | ||
if(JSEvents.inputEvent == null){ | ||
JSEvents.inputEvent = _malloc(eventSize); |
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.
No need to call _malloc
at all here since _realloc
works with null pointer as firs argument.
} | ||
else if(JSEvents.inputEventAllocatedSize < eventSize) { | ||
JSEvents.inputEvent = _realloc(JSEvents.inputEvent, eventSize); | ||
JSEvents.inputEventAllocatedSize = eventSize; |
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.
Maybe just call realloc
unconditionally and then you don't need to trace inputEventAllocatedSize
at all.
typedef struct EmscriptenInputEvent { | ||
bool isComposing; | ||
EM_UTF8 inputType[EM_HTML5_SHORT_STRING_LEN_BYTES]; | ||
EM_UTF8 data[]; |
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.
Perhaps worth adding a comment here. EM_UTF8 data[]; // variable size string; must be the final element of the struct
void* const userData) { | ||
printEvent(inputEvent, __func__); | ||
|
||
if (currentGoal != GoalInputSingleLetter) { |
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.
How about just assert(currentGoal == GoalInputSingleLetter)
here and elsewhere?
|
||
static void* const expectedUserDataPtr = (void*)0x13370001; | ||
|
||
static bool |
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.
Would it help keep the test simpler (more readable) if you just removed all the static
keywords? They don't really add anything to the test I think.
// * copy and paste: | ||
static const char* const inputType_insertFromPaste = "insertFromPaste"; | ||
// * drag and drop: | ||
static const char* const inputType_insertFromDrop = "insertFromDrop"; |
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.
Maybe drop the second const
here since it adds visual noise?
Would it be more readable to just write #define INPUTTYPE_INSERTTEXT "insertText"
?
Implements the HTML DOM 'input' event as suggested in #23523.