Skip to content
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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

alibabashack
Copy link

@alibabashack alibabashack commented Jan 31, 2025

Implements the HTML DOM 'input' event as suggested in #23523.

@alibabashack
Copy link
Author

alibabashack commented Jan 31, 2025

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:
test/runner interactive.test_html5_input

@alibabashack
Copy link
Author

alibabashack commented Jan 31, 2025

I think I need some help to interpret the CI result. I don't understand why test_offset_converter_pthread and test_pthread_wait64_notify tests fail. They seem to be unrelated to my work. Any help is much appreciated, thanks.

Edit: Oh, I see, looks main is broken...

@sbc100
Copy link
Collaborator

sbc100 commented Jan 31, 2025

I think I need some help to interpret the CI result. I don't understand why test_offset_converter_pthread and test_pthread_wait64_notify tests fail. They seem to be unrelated to my work. Any help is much appreciated, thanks.

Edit: Oh, I see, looks main is broken...

Those tests are known to be flaky. We can re-run the CI and they should pass.

@@ -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];
Copy link
Collaborator

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:

  1. 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.
  2. Use heap allocation. i.e stringToNewUTF8 on the JS side. It would then be incumbent upon the receiver to call free on the resulting pointer when they are done with it.

I guess (2) seems safer since it avoids a possible stack overflow.

Copy link
Author

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:

  1. 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
  2. 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.
  3. 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?

Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Author

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!

Copy link
Author

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.

src/lib/libhtml5.js Outdated Show resolved Hide resolved
src/lib/libhtml5.js Outdated Show resolved Hide resolved
src/lib/libhtml5.js Outdated Show resolved Hide resolved
src/lib/libhtml5.js Outdated Show resolved Hide resolved
src/lib/libhtml5.js Outdated Show resolved Hide resolved
src/lib/libhtml5.js Show resolved Hide resolved
test/test_html5_input.c Outdated Show resolved Hide resolved

#ifdef REPORT_RESULT
REPORT_RESULT(testStatus());
#endif
Copy link
Collaborator

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?

Copy link
Author

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.

src/lib/libhtml5.js Outdated Show resolved Hide resolved
@alibabashack alibabashack requested a review from sbc100 February 3, 2025 14:04
@alibabashack
Copy link
Author

I also added $registerInputEventCallback__proxy: 'sync', because it seems to be present on all other callback handlers. To be honest I have no idea what this does. Could someone explain? Thanks.

@alibabashack
Copy link
Author

rebased to latest master

Copy link
Collaborator

@sbc100 sbc100 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, with a few minor nits, and some comments about the test code.

@kripken and @juj, I don't know much about how the html5 API evolved or why this was not originally part of it. Does this addition look reasonable to you? @dschuff too.

var isComposing = e.isComposing;
const dataLengthWithTermination = lengthBytesUTF8(data) + 1;
const eventSize = {{{ C_STRUCTS.EmscriptenInputEvent.__size__ }}} + dataLengthWithTermination;
if(JSEvents.inputEvent == null){
Copy link
Collaborator

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 || '';
Copy link
Collaborator

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);
Copy link
Collaborator

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;
Copy link
Collaborator

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[];
Copy link
Collaborator

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) {
Copy link
Collaborator

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
Copy link
Collaborator

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";
Copy link
Collaborator

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"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants