[wasm][debugger] Add support for invoking getters on ValueTypes (#40548)
authorAnkit Jain <radical@gmail.com>
Sun, 9 Aug 2020 16:36:16 +0000 (12:36 -0400)
committerGitHub <noreply@github.com>
Sun, 9 Aug 2020 16:36:16 +0000 (11:36 -0500)
* [wasm][debugger][tests] Fix negative pointer tests

* [wasm][debugger][tests] Fix test to correctly check the valuetype local

test: `CheckUpdatedValueTypeFieldsOnResume`

* [wasm][debugger][tests] Make value checks consistent

- In some places we weren't checking for the `description` property
- and this hid a bug where sometimes that property wasn't added (eg. for
numbers)
- Instead, we were working around that by "fixing it up" later

- Now, we use the same checks for `Check{Number,String,*}` API, and the
`CheckValue/CheckProps` API used with `TNumber` etc.

- So, this commit:
- fixes the checks, and the tests
- and fixes the bug

* [wasm][debugger] Add new `id` types, which have associated properties

- these are of the form `dotnet:${scheme}:{id-args-object}`
- Examples:
    - `dotnet:valuetype:{ containerId: 4 }`
    - `dotnet:valuetype:{ num: 2 }` - the `num` field is
    autogenerated if no id-args are provided. This gets used when
    valuetypes are expanded.

- `this._id_table [id-string]` has associated property objects for every
`id`
    - This might contain, for example, `klass` pointer, and base64
    representation of a valuetype

* [wasm][debugger] Update valuetype code to use the new `id`s

* [wasm][debugger] Simplify array API in `mini-wasm-debugger.c`

.. to use a single function to get details of the full array, and
individual elements.

* [wasm][debugger] library_mono.js: improvements to valuetype code

- Allow `_new_id` to update properties for existing objectIds
- Extract valuetype id assigment code to a separate function

* [wasm][debugger] mini-wasm-debugger.c- extract object id lookup into a function

* [wasm][debugger][tests] Rename method param to be self descriptive

* [wasm][debugger][tests] Rework cfo test for getters

- add some new getters to the test classes
- this will become useful in subsequent commits that add support for
invoking getters on valuetypes

* [wasm][debugger][tests] Improve valuetype locals/method args tests

- this also becomes useful in subsequent commits which enable invoking
getters on valuetypes

* [wasm][debugger] Add support for invoking getters on valuetypes

- keep a copy of the value bytes, and the klass pointer
- this allows being able to invoke getters on such a valuetype, at a
later point
- This allows getters like `DateTime.Date`, which has the type
`DateTime`

* [wasm][debugger] mono.js: fix warnings

.. and replace `var` with `let`, or `const`, where appropriate.

* [wasm][debugger] mono.js: _split_object_id -> _parse_object_id

* [wasm][debugger] Streamline accessing exported debugger.c functions

.. especially the ones that return data in `MONO.var_info`.

To use:

1. `this._register_c_var_fn ('mono_wasm_get_object_properties', 'bool', [ 'number', 'bool' ]);`
2. Now, this function can be called as `this.mono_wasm_get_object_properties_info (.. )`
    - returns `res` which has the contents of `MONO.var_info`, after running
    `_fixup_name_value_objects` on it.

* [wasm][debugger] Return errors from debugger.c's details functions

- functions like those for getting object/vt properties, can fail, for
example, if the objectId is invalid.
- We now return that bool result, and that gets surfaced to the caller

- This will also help to differentiate the case where the result of such
a function was a failure vs just an empty result

* [wasm][debugger] Small checks on inputs, and some negative tests

- These tests don't actually depend on the error message, and we don't
have another to way to differentiate why a command might have failed
with an exception. So, right now, they will pass as long as the commands
fail as expected.

- Future TODO: return `error`, instead of exception details for issues
in `mono.js`, like incorrect input, invalid id etc, and update these
tests accordingly.

* Update src/mono/mono/mini/mini-wasm-debugger.c

Co-authored-by: Larry Ewing <lewing@microsoft.com>
* Remove description checking from TString

Co-authored-by: Larry Ewing <lewing@microsoft.com>
src/mono/mono/mini/mini-wasm-debugger.c
src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs
src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs
src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs
src/mono/wasm/debugger/DebuggerTestSuite/Support.cs
src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs
src/mono/wasm/debugger/tests/debugger-cfo-test.cs
src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs
src/mono/wasm/debugger/tests/other.js
src/mono/wasm/runtime/library_mono.js

index 90855b5..66608a7 100644 (file)
@@ -37,24 +37,21 @@ EMSCRIPTEN_KEEPALIVE int mono_wasm_set_breakpoint (const char *assembly_name, in
 EMSCRIPTEN_KEEPALIVE int mono_wasm_remove_breakpoint (int bp_id);
 EMSCRIPTEN_KEEPALIVE int mono_wasm_current_bp_id (void);
 EMSCRIPTEN_KEEPALIVE void mono_wasm_enum_frames (void);
-EMSCRIPTEN_KEEPALIVE void mono_wasm_get_var_info (int scope, int* pos, int len);
+EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_local_vars (int scope, int* pos, int len);
 EMSCRIPTEN_KEEPALIVE void mono_wasm_clear_all_breakpoints (void);
 EMSCRIPTEN_KEEPALIVE int mono_wasm_setup_single_step (int kind);
 EMSCRIPTEN_KEEPALIVE int mono_wasm_pause_on_exceptions (int state);
-EMSCRIPTEN_KEEPALIVE void mono_wasm_get_object_properties (int object_id, gboolean expand_value_types);
-EMSCRIPTEN_KEEPALIVE void mono_wasm_get_array_values (int object_id);
-EMSCRIPTEN_KEEPALIVE void mono_wasm_get_array_value_expanded (int object_id, int idx);
-EMSCRIPTEN_KEEPALIVE void mono_wasm_invoke_getter_on_object (int object_id, const char* name);
-EMSCRIPTEN_KEEPALIVE void mono_wasm_get_deref_ptr_value (void *value_addr, MonoClass *klass);
+EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_object_properties (int object_id, gboolean expand_value_types);
+EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_array_values (int object_id, int start_idx, int count, gboolean expand_value_types);
+EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_invoke_getter_on_object (int object_id, const char* name);
+EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_invoke_getter_on_value (void *value, MonoClass *klass, const char *name);
+EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_deref_ptr_value (void *value_addr, MonoClass *klass);
 
 //JS functions imported that we use
 extern void mono_wasm_add_frame (int il_offset, int method_token, const char *assembly_name, const char *method_name);
 extern void mono_wasm_fire_bp (void);
 extern void mono_wasm_fire_exception (int exception_obj_id, const char* message, const char* class_name, gboolean uncaught);
 extern void mono_wasm_add_obj_var (const char*, const char*, guint64);
-extern void mono_wasm_add_value_type_unexpanded_var (const char*, const char*);
-extern void mono_wasm_begin_value_type_var (const char*, const char*);
-extern void mono_wasm_end_value_type_var (void);
 extern void mono_wasm_add_enum_var (const char*, const char*, guint64);
 extern void mono_wasm_add_func_var (const char*, const char*, guint64);
 extern void mono_wasm_add_properties_var (const char*, gint32);
@@ -631,6 +628,22 @@ mono_wasm_current_bp_id (void)
        return evt->id;
 }
 
+static MonoObject*
+get_object_from_id (int objectId)
+{
+       ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId));
+       if (!ref) {
+               DEBUG_PRINTF (2, "get_object_from_id !ref: %d\n", objectId);
+               return NULL;
+       }
+
+       MonoObject *obj = mono_gchandle_get_target_internal (ref->handle);
+       if (!obj)
+               DEBUG_PRINTF (2, "get_object_from_id !obj: %d\n", objectId);
+
+       return obj;
+}
+
 static gboolean
 list_frames (MonoStackFrameInfo *info, MonoContext *ctx, gpointer data)
 {
@@ -968,17 +981,28 @@ static gboolean describe_value(MonoType * type, gpointer addr, gboolean expandVa
 
                                mono_wasm_add_enum_var (class_name, enum_members->str, value__);
                                g_string_free (enum_members, TRUE);
-                       } else if (expandValueType) {
-                               char *to_string_val = get_to_string_description (class_name, klass, addr);
-                               mono_wasm_begin_value_type_var (class_name, to_string_val);
-                               g_free (to_string_val);
-
-                               // FIXME: isAsyncLocalThis
-                               describe_object_properties_for_klass ((MonoObject*)addr, klass, FALSE, expandValueType);
-                               mono_wasm_end_value_type_var ();
                        } else {
                                char *to_string_val = get_to_string_description (class_name, klass, addr);
-                               mono_wasm_add_value_type_unexpanded_var (class_name, to_string_val);
+
+                               if (expandValueType) {
+                                       int32_t size = mono_class_value_size (klass, NULL);
+                                       void *value_buf = g_malloc0 (size);
+                                       mono_value_copy_internal (value_buf, addr, klass);
+
+                                       EM_ASM ({
+                                               MONO.mono_wasm_add_typed_value ($0, $1, { toString: $2, value_addr: $3, value_size: $4, klass: $5 });
+                                       }, "begin_vt", class_name, to_string_val, value_buf, size, klass);
+
+                                       g_free (value_buf);
+
+                                       // FIXME: isAsyncLocalThis
+                                       describe_object_properties_for_klass (addr, klass, FALSE, expandValueType);
+                                       mono_wasm_add_typed_value ("end_vt", NULL, 0);
+                               } else {
+                                       EM_ASM ({
+                                               MONO.mono_wasm_add_typed_value ($0, $1, { toString: $2 });
+                                       }, "unexpanded_vt", class_name, to_string_val);
+                               }
                                g_free (to_string_val);
                        }
                        g_free (class_name);
@@ -1036,7 +1060,7 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs
        gboolean is_valuetype;
        int pnum;
        char *klass_name;
-       gboolean getters_allowed;
+       gboolean auto_invoke_getters;
 
        g_assert (klass);
        is_valuetype = m_class_is_valuetype(klass);
@@ -1069,7 +1093,7 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs
        }
 
        klass_name = mono_class_full_name (klass);
-       getters_allowed = are_getters_allowed (klass_name);
+       auto_invoke_getters = are_getters_allowed (klass_name);
 
        iter = NULL;
        pnum = 0;
@@ -1081,29 +1105,24 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs
                        mono_wasm_add_properties_var (p->name, pnum);
                        sig = mono_method_signature_internal (p->get);
 
-                       // automatic properties will get skipped
-                       if (!getters_allowed) {
+                       gboolean vt_self_type_getter = is_valuetype && mono_class_from_mono_type_internal (sig->ret) == klass;
+                       if (auto_invoke_getters && !vt_self_type_getter) {
+                               invoke_and_describe_getter_value (obj, p);
+                       } else {
                                // not allowed to call the getter here
                                char *ret_class_name = mono_class_full_name (mono_class_from_mono_type_internal (sig->ret));
 
-                               // getters not supported for valuetypes, yet
-                               gboolean invokable = !is_valuetype && sig->param_count == 0;
+                               gboolean invokable = sig->param_count == 0;
                                mono_wasm_add_typed_value ("getter", ret_class_name, invokable);
 
                                g_free (ret_class_name);
                                continue;
                        }
-
-                       if (is_valuetype && mono_class_from_mono_type_internal (sig->ret) == klass) {
-                               // Property of the same valuetype, avoid endlessly recursion!
-                               mono_wasm_add_typed_value ("getter", klass_name, 0);
-                               continue;
-                       }
-
-                       invoke_and_describe_getter_value (obj, p);
                }
                pnum ++;
        }
+
+       g_free (klass_name);
 }
 
 /*
@@ -1132,17 +1151,10 @@ static gboolean
 describe_object_properties (guint64 objectId, gboolean isAsyncLocalThis, gboolean expandValueType)
 {
        DEBUG_PRINTF (2, "describe_object_properties %llu\n", objectId);
-       ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId));
-       if (!ref) {
-               DEBUG_PRINTF (2, "describe_object_properties !ref\n");
-               return FALSE;
-       }
 
-       MonoObject *obj = mono_gchandle_get_target_internal (ref->handle);
-       if (!obj) {
-               DEBUG_PRINTF (2, "describe_object_properties !obj\n");
+       MonoObject *obj = get_object_from_id (objectId);
+       if (!obj)
                return FALSE;
-       }
 
        if (m_class_is_delegate (mono_object_class (obj))) {
                // delegates get the same id format as regular objects
@@ -1155,21 +1167,13 @@ describe_object_properties (guint64 objectId, gboolean isAsyncLocalThis, gboolea
 }
 
 static gboolean
-invoke_getter_on_object (guint64 objectId, const char *name)
+invoke_getter (void *obj_or_value, MonoClass *klass, const char *name)
 {
-       ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId));
-       if (!ref) {
-               DEBUG_PRINTF (1, "invoke_getter_on_object no objRef found for id %llu\n", objectId);
-               return FALSE;
-       }
-
-       MonoObject *obj = mono_gchandle_get_target_internal (ref->handle);
-       if (!obj) {
-               DEBUG_PRINTF (1, "invoke_getter_on_object !obj\n");
+       if (!obj_or_value || !klass || !name) {
+               DEBUG_PRINTF (2, "invoke_getter: none of the arguments can be null");
                return FALSE;
        }
 
-       MonoClass *klass = mono_object_class (obj);
        gpointer iter = NULL;
        MonoProperty *p;
        while ((p = mono_class_get_properties (klass, &iter))) {
@@ -1177,7 +1181,7 @@ invoke_getter_on_object (guint64 objectId, const char *name)
                if (!p->get->name || strcasecmp (p->name, name) != 0)
                        continue;
 
-               invoke_and_describe_getter_value (obj, p);
+               invoke_and_describe_getter_value (obj_or_value, p);
                return TRUE;
        }
 
@@ -1185,50 +1189,48 @@ invoke_getter_on_object (guint64 objectId, const char *name)
 }
 
 static gboolean 
-describe_array_values (guint64 objectId)
+describe_array_values (guint64 objectId, int startIdx, int count, gboolean expandValueType)
 {
+       if (count == 0)
+               return TRUE;
+
        int esize;
        gpointer elem;
-       ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId));
-       if (!ref) {
+       MonoArray *arr = (MonoArray*) get_object_from_id (objectId);
+       if (!arr)
                return FALSE;
-       }
-       MonoArray *arr = (MonoArray *)mono_gchandle_get_target_internal (ref->handle);
-       MonoObject *obj = &arr->obj;
-       if (!obj) {
+
+       MonoClass *klass = mono_object_class (arr);
+       MonoTypeEnum type = m_class_get_byval_arg (klass)->type;
+       if (type != MONO_TYPE_SZARRAY && type != MONO_TYPE_ARRAY) {
+               DEBUG_PRINTF (1, "describe_array_values: object is not an array. type: 0x%x\n", type);
                return FALSE;
        }
-       esize = mono_array_element_size (obj->vtable->klass);
-       for (int i = 0; i < arr->max_length; i++) {
-               mono_wasm_add_array_item(i);
-               elem = (gpointer*)((char*)arr->vector + (i * esize));
-               describe_value (m_class_get_byval_arg (m_class_get_element_class (arr->obj.vtable->klass)), elem, FALSE);
+
+       int len = arr->max_length;
+       if (len == 0 && startIdx == 0 && count <= 0) {
+               // Nothing to do
+               return TRUE;
        }
-       return TRUE;
-}
 
-/* Expands valuetypes */
-static gboolean
-describe_array_value_expanded (guint64 objectId, guint64 idx)
-{
-       int esize;
-       gpointer elem;
-       ObjRef *ref = (ObjRef *)g_hash_table_lookup (objrefs, GINT_TO_POINTER (objectId));
-       if (!ref) {
+       if (startIdx < 0 || (len > 0 && startIdx >= len)) {
+               DEBUG_PRINTF (1, "describe_array_values: invalid startIdx (%d) for array of length %d\n", startIdx, len);
                return FALSE;
        }
-       MonoArray *arr = (MonoArray *)mono_gchandle_get_target_internal (ref->handle);
-       MonoObject *obj = &arr->obj;
-       if (!obj) {
+
+       if (count > 0 && (startIdx + count) > len) {
+               DEBUG_PRINTF (1, "describe_array_values: invalid count (%d) for startIdx: %d, and array of length %d\n", count, startIdx, len);
                return FALSE;
        }
-       if (idx >= arr->max_length)
-               return FALSE;
 
-       esize = mono_array_element_size (obj->vtable->klass);
-       elem = (gpointer*)((char*)arr->vector + (idx * esize));
-       describe_value (m_class_get_byval_arg (m_class_get_element_class (arr->obj.vtable->klass)), elem, TRUE);
+       esize = mono_array_element_size (klass);
+       int endIdx = count < 0 ? len : startIdx + count;
 
+       for (int i = startIdx; i < endIdx; i ++) {
+               mono_wasm_add_array_item(i);
+               elem = (gpointer*)((char*)arr->vector + (i * esize));
+               describe_value (m_class_get_byval_arg (m_class_get_element_class (klass)), elem, expandValueType);
+       }
        return TRUE;
 }
 
@@ -1331,22 +1333,23 @@ describe_variables_on_frame (MonoStackFrameInfo *info, MonoContext *ctx, gpointe
        return TRUE;
 }
 
-EMSCRIPTEN_KEEPALIVE void
+EMSCRIPTEN_KEEPALIVE gboolean
 mono_wasm_get_deref_ptr_value (void *value_addr, MonoClass *klass)
 {
        MonoType *type = m_class_get_byval_arg (klass);
        if (type->type != MONO_TYPE_PTR && type->type != MONO_TYPE_FNPTR) {
                DEBUG_PRINTF (2, "BUG: mono_wasm_get_deref_ptr_value: Expected to get a ptr type, but got 0x%x\n", type->type);
-               return;
+               return FALSE;
        }
 
        mono_wasm_add_properties_var ("deref", -1);
        describe_value (type->data.type, value_addr, TRUE);
+       return TRUE;
 }
 
 //FIXME this doesn't support getting the return value pseudo-var
-EMSCRIPTEN_KEEPALIVE void
-mono_wasm_get_var_info (int scope, int* pos, int len)
+EMSCRIPTEN_KEEPALIVE gboolean
+mono_wasm_get_local_vars (int scope, int* pos, int len)
 {
        FrameDescData data;
        data.target_frame = scope;
@@ -1355,37 +1358,51 @@ mono_wasm_get_var_info (int scope, int* pos, int len)
        data.pos = pos;
 
        mono_walk_stack_with_ctx (describe_variables_on_frame, NULL, MONO_UNWIND_NONE, &data);
+
+       return TRUE;
 }
 
-EMSCRIPTEN_KEEPALIVE void
+EMSCRIPTEN_KEEPALIVE gboolean
 mono_wasm_get_object_properties (int object_id, gboolean expand_value_types)
 {
        DEBUG_PRINTF (2, "getting properties of object %d\n", object_id);
 
-       describe_object_properties (object_id, FALSE, expand_value_types);
+       return describe_object_properties (object_id, FALSE, expand_value_types);
 }
 
-EMSCRIPTEN_KEEPALIVE void
-mono_wasm_get_array_values (int object_id)
+EMSCRIPTEN_KEEPALIVE gboolean
+mono_wasm_get_array_values (int object_id, int start_idx, int count, gboolean expand_value_types)
 {
-       DEBUG_PRINTF (2, "getting array values %d\n", object_id);
+       DEBUG_PRINTF (2, "getting array values %d, startIdx: %d, count: %d, expandValueType: %d\n", object_id, start_idx, count, expand_value_types);
 
-       describe_array_values(object_id);
+       return describe_array_values (object_id, start_idx, count, expand_value_types);
 }
 
-EMSCRIPTEN_KEEPALIVE void
-mono_wasm_get_array_value_expanded (int object_id, int idx)
+EMSCRIPTEN_KEEPALIVE gboolean
+mono_wasm_invoke_getter_on_object (int object_id, const char* name)
 {
-       DEBUG_PRINTF (2, "getting array value %d for idx %d\n", object_id, idx);
+       MonoObject *obj = get_object_from_id (object_id);
+       if (!obj)
+               return FALSE;
 
-       describe_array_value_expanded (object_id, idx);
+       return invoke_getter (obj, mono_object_class (obj), name);
 }
 
-EMSCRIPTEN_KEEPALIVE void
-mono_wasm_invoke_getter_on_object (int object_id, const char* name)
+EMSCRIPTEN_KEEPALIVE gboolean
+mono_wasm_invoke_getter_on_value (void *value, MonoClass *klass, const char *name)
 {
-       invoke_getter_on_object (object_id, name);
+       DEBUG_PRINTF (2, "mono_wasm_invoke_getter_on_value: v: %p klass: %p, name: %s\n", value, klass, name);
+       if (!klass || !value)
+               return FALSE;
+
+       if (!m_class_is_valuetype (klass)) {
+               DEBUG_PRINTF (2, "mono_wasm_invoke_getter_on_value: klass is not a valuetype. name: %s\n", mono_class_full_name (klass));
+               return FALSE;
+       }
+
+       return invoke_getter (value, klass, name);
 }
+
 // Functions required by debugger-state-machine.
 gsize
 mono_debugger_tls_thread_id (DebuggerTlsData *debuggerTlsData)
index 022e41c..f9319a7 100644 (file)
@@ -26,7 +26,7 @@ namespace DebuggerTests
             etype_name: "int",
             local_var_name_prefix: "int",
             array : new [] { TNumber(4), TNumber(70), TNumber(1) },
-            array_elements : null,
+            array_elem_props: null,
             test_prev_frame : test_prev_frame,
             frame_idx : frame_idx,
             use_cfo : use_cfo);
@@ -47,7 +47,7 @@ namespace DebuggerTests
                 TValueType("DebuggerTests.Point"),
                     TValueType("DebuggerTests.Point"),
             },
-            array_elements : new []
+            array_elem_props: new []
             {
                 TPoint(5, -2, "point_arr#Id#0", "Green"),
                     TPoint(123, 0, "point_arr#Id#1", "Blue")
@@ -73,7 +73,7 @@ namespace DebuggerTests
                     TObject("DebuggerTests.SimpleClass", is_null : true),
                     TObject("DebuggerTests.SimpleClass")
             },
-            array_elements : new []
+            array_elem_props: new []
             {
                 TSimpleClass(5, -2, "class_arr#Id#0", "Green"),
                     null, // Element is null
@@ -100,7 +100,7 @@ namespace DebuggerTests
                     TObject("DebuggerTests.GenericClass<int>"),
                     TObject("DebuggerTests.GenericClass<int>")
             },
-            array_elements : new []
+            array_elem_props : new []
             {
                 null, // Element is null
                 new
@@ -136,7 +136,7 @@ namespace DebuggerTests
                 TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
                     TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>")
             },
-            array_elements : new []
+            array_elem_props : new []
             {
                 new
                 {
@@ -171,7 +171,7 @@ namespace DebuggerTests
                 TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point[]>"),
                     TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point[]>")
             },
-            array_elements : new []
+            array_elem_props : new []
             {
                 new
                 {
@@ -199,7 +199,7 @@ namespace DebuggerTests
             use_cfo : use_cfo);
 
         async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, string method_name, string etype_name,
-            string local_var_name_prefix, object[] array, object[] array_elements,
+            string local_var_name_prefix, object[] array, object[] array_elem_props,
             bool test_prev_frame = false, int frame_idx = 0, bool use_cfo = false)
         {
             var insp = new Inspector();
@@ -223,8 +223,8 @@ namespace DebuggerTests
 
                 var locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
                 Assert.Equal(4, locals.Count());
-                CheckArray(locals, $"{local_var_name_prefix}_arr", $"{etype_name}[]");
-                CheckArray(locals, $"{local_var_name_prefix}_arr_empty", $"{etype_name}[]");
+                CheckArray(locals, $"{local_var_name_prefix}_arr", $"{etype_name}[]", array?.Length ?? 0);
+                CheckArray(locals, $"{local_var_name_prefix}_arr_empty", $"{etype_name}[]", 0);
                 CheckObject(locals, $"{local_var_name_prefix}_arr_null", $"{etype_name}[]", is_null : true);
                 CheckBool(locals, "call_other", test_prev_frame);
 
@@ -250,13 +250,13 @@ namespace DebuggerTests
 
                 await CheckProps(prefix_arr, array, local_arr_name);
 
-                if (array_elements?.Length > 0)
+                if (array_elem_props?.Length > 0)
                 {
-                    for (int i = 0; i < array_elements.Length; i++)
+                    for (int i = 0; i < array_elem_props.Length; i++)
                     {
                         var i_str = i.ToString();
                         var label = $"{local_var_name_prefix}_arr[{i}]";
-                        if (array_elements[i] == null)
+                        if (array_elem_props[i] == null)
                         {
                             var act_i = prefix_arr.FirstOrDefault(jt => jt["name"]?.Value<string>() == i_str);
                             Assert.True(act_i != null, $"[{label}] Couldn't find array element [{i_str}]");
@@ -265,7 +265,7 @@ namespace DebuggerTests
                         }
                         else
                         {
-                            await CompareObjectPropertiesFor(prefix_arr, i_str, array_elements[i], label : label);
+                            await CompareObjectPropertiesFor(prefix_arr, i_str, array_elem_props[i], label : label);
                         }
                     }
                 }
@@ -601,5 +601,95 @@ namespace DebuggerTests
             });
         }
 
+        [Fact]
+        public async Task InvalidArrayId() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.Container", "PlaceholderMethod", 1, "PlaceholderMethod",
+            "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers'); }, 1);",
+            wait_for_event_fn : async(pause_location) =>
+            {
+
+                int frame_idx = 1;
+                var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+                var c_obj = GetAndAssertObjectWithName(frame_locals, "c");
+                var c_obj_id = c_obj["value"] ? ["objectId"]?.Value<string>();
+                Assert.NotNull(c_obj_id);
+
+                // Invalid format
+                await GetProperties("dotnet:array:4123", expect_ok : false);
+
+                // Invalid object id
+                await GetProperties("dotnet:array:{ \"arrayId\": 234980 }", expect_ok : false);
+
+                // Trying to access object as an array
+                if (!DotnetObjectId.TryParse (c_obj_id, out var id) || id.Scheme != "object")
+                    Assert.True(false, "Unexpected object id format. Maybe this test is out of sync with the object id format in library_mono.js?");
+
+                if (!int.TryParse(id.Value, out var idNum))
+                    Assert.True(false, "Expected a numeric value part of the object id: {c_obj_id}");
+                await GetProperties($"dotnet:array:{{\"arrayId\":{idNum}}}", expect_ok : false);
+            });
+
+        [Fact]
+        public async Task InvalidValueTypeArrayIndex() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.Container", "PlaceholderMethod", 1, "PlaceholderMethod",
+            "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers'); }, 1);",
+            locals_fn : async(locals) =>
+            {
+                var this_obj = GetAndAssertObjectWithName(locals, "this");
+                var c_obj = GetAndAssertObjectWithName(await GetProperties(this_obj["value"]["objectId"].Value<string>()), "c");
+                var c_obj_id = c_obj["value"] ? ["objectId"]?.Value<string>();
+                Assert.NotNull(c_obj_id);
+
+                var c_props = await GetProperties(c_obj_id);
+
+                var pf_arr = GetAndAssertObjectWithName(c_props, "PointsField");
+                var pf_arr_elems = await GetProperties(pf_arr["value"]["objectId"].Value<string>());
+
+                if (!DotnetObjectId.TryParse(pf_arr_elems[0]["value"] ? ["objectId"]?.Value<string>(), out var id))
+                    Assert.True(false, "Couldn't parse objectId for PointsFields' elements");
+
+                AssertEqual("valuetype", id.Scheme, "Expected a valuetype id");
+                var id_args = id.ValueAsJson;
+                Assert.True(id_args["arrayId"] != null, "ObjectId format for array seems to have changed. Expected to find 'arrayId' in the value. Update this test");
+                Assert.True(id_args != null, "Expected to get a json as the value part of {id}");
+
+                // Try one valid query, to confirm that the id format hasn't changed!
+                id_args["arrayIdx"] = 0;
+                await GetProperties($"dotnet:valuetype:{id_args.ToString (Newtonsoft.Json.Formatting.None)}", expect_ok : true);
+
+                id_args["arrayIdx"] = 12399;
+                await GetProperties($"dotnet:valuetype:{id_args.ToString (Newtonsoft.Json.Formatting.None)}", expect_ok : false);
+
+                id_args["arrayIdx"] = -1;
+                await GetProperties($"dotnet:valuetype:{id_args.ToString (Newtonsoft.Json.Formatting.None)}", expect_ok : false);
+
+                id_args["arrayIdx"] = "qwe";
+                await GetProperties($"dotnet:valuetype:{id_args.ToString (Newtonsoft.Json.Formatting.None)}", expect_ok : false);
+            });
+
+        [Fact]
+        public async Task InvalidAccessors() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.Container", "PlaceholderMethod", 1, "PlaceholderMethod",
+            "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers'); }, 1);",
+            locals_fn : async(locals) =>
+            {
+                var this_obj = GetAndAssertObjectWithName(locals, "this");
+                var c_obj = GetAndAssertObjectWithName(await GetProperties(this_obj["value"]["objectId"].Value<string>()), "c");
+                var c_obj_id = c_obj["value"] ? ["objectId"]?.Value<string>();
+                Assert.NotNull(c_obj_id);
+
+                var c_props = await GetProperties(c_obj_id);
+
+                var pf_arr = GetAndAssertObjectWithName(c_props, "PointsField");
+
+                var invalid_accessors = new object[] { "NonExistant", "10000", "-2", 10000, -2, null, String.Empty };
+                foreach (var invalid_accessor in invalid_accessors)
+                {
+                    // var res = await InvokeGetter (JObject.FromObject (new { value = new { objectId = obj_id } }), invalid_accessor, expect_ok: true);
+                    var res = await InvokeGetter(pf_arr, invalid_accessor, expect_ok : true);
+                    AssertEqual("undefined", res.Value["result"] ? ["type"]?.ToString(), "Expected to get undefined result for non-existant accessor");
+                }
+            });
+
     }
-}
\ No newline at end of file
+}
index 5c66bb2..3014c6c 100644 (file)
@@ -481,6 +481,36 @@ namespace DebuggerTests
                 // callFunctionOn
                 result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
                 await CheckValue(result.Value["result"], TNumber(5), "cfo-res");
+
+                cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = "function () { return 'test value'; }",
+                        objectId = obj_id
+                });
+
+                // value of @returnByValue doesn't matter, as the returned value
+                // is a primitive
+                if (return_by_val)
+                    cfo_args["returnByValue"] = return_by_val;
+
+                // callFunctionOn
+                result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+                await CheckValue(result.Value["result"], JObject.FromObject(new { type = "string", value = "test value" }), "cfo-res");
+
+                cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = "function () { return null; }",
+                        objectId = obj_id
+                });
+
+                // value of @returnByValue doesn't matter, as the returned value
+                // is a primitive
+                if (return_by_val)
+                    cfo_args["returnByValue"] = return_by_val;
+
+                // callFunctionOn
+                result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+                await CheckValue(result.Value["result"], JObject.Parse("{ type: 'object', subtype: 'null', value: null }"), "cfo-res");
             });
         }
 
@@ -536,7 +566,7 @@ namespace DebuggerTests
             });
         }
 
-        public static TheoryData<string, string, int, int, string, Func<string[], object>, bool> GettersTestData(bool use_cfo) => new TheoryData<string, string, int, int, string, Func<string[], object>, bool>
+        public static TheoryData<string, string, int, int, string, Func<string[], object>, string, bool> GettersTestData(string local_name, bool use_cfo) => new TheoryData<string, string, int, int, string, Func<string[], object>, string, bool>
         {
             // Chrome sends this one
             {
@@ -546,6 +576,7 @@ namespace DebuggerTests
                 12,
                 "function invokeGetter(arrayStr){ let result=this; const properties=JSON.parse(arrayStr); for(let i=0,n=properties.length;i<n;++i){ result=result[properties[i]]; } return result; }",
                 (arg_strs) => JArray.FromObject(arg_strs).ToString(),
+                local_name,
                 use_cfo
             },
             {
@@ -555,6 +586,7 @@ namespace DebuggerTests
                 12,
                 "function invokeGetter(arrayStr){ let result=this; const properties=JSON.parse(arrayStr); for(let i=0,n=properties.length;i<n;++i){ result=result[properties[i]]; } return result; }",
                 (arg_strs) => JArray.FromObject(arg_strs).ToString(),
+                local_name,
                 use_cfo
             },
 
@@ -566,6 +598,7 @@ namespace DebuggerTests
                 12,
                 "function(e){return this[e]}",
                 (args_str) => args_str?.Length > 0 ? args_str[0] : String.Empty,
+                local_name,
                 use_cfo
             },
             {
@@ -575,14 +608,17 @@ namespace DebuggerTests
                 12,
                 "function(e){return this[e]}",
                 (args_str) => args_str?.Length > 0 ? args_str[0] : String.Empty,
+                local_name,
                 use_cfo
             }
         };
 
         [Theory]
-        [MemberData(nameof(GettersTestData), parameters : false)]
-        [MemberData(nameof(GettersTestData), parameters : true)]
-        public async Task PropertyGettersOnObjectsTest(string eval_fn, string method_name, int line, int col, string cfo_fn, Func<string[], object> get_args_fn, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+        [MemberData(nameof(GettersTestData), "ptd", false)]
+        [MemberData(nameof(GettersTestData), "ptd", true)]
+        [MemberData(nameof (GettersTestData), "swp", false)]
+        [MemberData(nameof (GettersTestData), "swp", true)]
+        public async Task PropertyGettersTest(string eval_fn, string method_name, int line, int col, string cfo_fn, Func<string[], object> get_args_fn, string local_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
             "dotnet://debugger-test.dll/debugger-cfo-test.cs", line, col,
             method_name,
             $"window.setTimeout(function() {{ {eval_fn} }}, 1);",
@@ -590,55 +626,56 @@ namespace DebuggerTests
             wait_for_event_fn : async(pause_location) =>
             {
                 var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
-                var dt = new DateTime(10, 9, 8, 7, 6, 5);
 
                 await CheckProps(frame_locals, new
                 {
                     ptd = TObject("DebuggerTests.ClassWithProperties"),
-                        swp = TObject("DebuggerTests.StructWithProperties"),
+                    swp = TObject("DebuggerTests.StructWithProperties")
                 }, "locals#0");
 
-                var ptd = GetAndAssertObjectWithName(frame_locals, "ptd");
+                var obj = GetAndAssertObjectWithName(frame_locals, local_name);
 
-                var ptd_props = await GetProperties(ptd?["value"] ? ["objectId"]?.Value<string>());
-                await CheckProps(ptd_props, new
+                var dt = new DateTime(4, 5, 6, 7, 8, 9);
+                var obj_props = await GetProperties(obj?["value"] ? ["objectId"]?.Value<string>());
+                await CheckProps(obj_props, new
                 {
-                    Int = TGetter("Int"),
-                        String = TGetter("String"),
-                        DT = TGetter("DT"),
-                        IntArray = TGetter("IntArray"),
-                        DTArray = TGetter("DTArray")
-                }, "ptd", num_fields : 7);
+                    V           = TNumber(0xDEADBEEF),
+                    Int         = TGetter("Int"),
+                    String      = TGetter("String"),
+                    DT          = TGetter("DT"),
+                    IntArray    = TGetter("IntArray"),
+                    DTArray     = TGetter("DTArray"),
+                    StringField = TString(null),
+
+                    // Auto properties show w/o getters, because they have
+                    // a backing field
+                    DTAutoProperty = TValueType("System.DateTime", dt.ToString())
+                }, local_name);
 
                 // Automatic properties don't have invokable getters, because we can get their
                 // value from the backing field directly
                 {
-                    dt = new DateTime(4, 5, 6, 7, 8, 9);
-                    var dt_auto_props = await GetObjectOnLocals(ptd_props, "DTAutoProperty");
-                    await CheckDateTime(ptd_props, "DTAutoProperty", dt);
+                    var dt_auto_props = await GetObjectOnLocals(obj_props, "DTAutoProperty");
+                    await CheckDateTime(obj_props, "DTAutoProperty", dt);
                 }
 
                 // Invoke getters, and check values
 
-                var res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "Int" }));
-                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
-                await CheckValue(res.Value["result"], JObject.FromObject(new { type = "number", value = 5 }), "ptd.Int");
+                dt = new DateTime(3, 4, 5, 6, 7, 8);
+                var res = await InvokeGetter(obj, get_args_fn(new [] { "Int" }), cfo_fn);
+                await CheckValue(res.Value["result"], JObject.FromObject(new { type = "number", value = (0xDEADBEEF + (uint) dt.Month) }), $"{local_name}.Int");
 
-                res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "String" }));
-                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
-                await CheckValue(res.Value["result"], JObject.FromObject(new { type = "string", value = "foobar" }), "ptd.String");
+                res = await InvokeGetter(obj, get_args_fn(new [] { "String" }), cfo_fn);
+                await CheckValue(res.Value["result"], JObject.FromObject(new { type = "string", value = $"String property, V: 0xDEADBEEF" }), $"{local_name}.String");
 
-                dt = new DateTime(3, 4, 5, 6, 7, 8);
-                res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "DT" }));
-                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
-                await CheckValue(res.Value["result"], TValueType("System.DateTime", dt.ToString()), "ptd.DT");
+                res = await InvokeGetter(obj, get_args_fn(new [] { "DT" }), cfo_fn);
+                await CheckValue(res.Value["result"], TValueType("System.DateTime", dt.ToString()), $"{local_name}.DT");
                 await CheckDateTimeValue(res.Value["result"], dt);
 
                 // Check arrays through getters
 
-                res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "IntArray" }));
-                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
-                await CheckValue(res.Value["result"], TArray("int[]", 2), "ptd.IntArray");
+                res = await InvokeGetter(obj, get_args_fn(new [] { "IntArray" }), cfo_fn);
+                await CheckValue(res.Value["result"], TArray("int[]", 2), $"{local_name}.IntArray");
                 {
                     var arr_elems = await GetProperties(res.Value["result"] ? ["objectId"]?.Value<string>());
                     var exp_elems = new []
@@ -647,12 +684,11 @@ namespace DebuggerTests
                         TNumber(20)
                     };
 
-                    await CheckProps(arr_elems, exp_elems, "ptd.IntArray");
+                    await CheckProps(arr_elems, exp_elems, $"{local_name}.IntArray");
                 }
 
-                res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "DTArray" }));
-                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
-                await CheckValue(res.Value["result"], TArray("System.DateTime[]", 2), "ptd.DTArray");
+                res = await InvokeGetter(obj, get_args_fn(new [] { "DTArray" }), cfo_fn);
+                await CheckValue(res.Value["result"], TArray("System.DateTime[]", 2), $"{local_name}.DTArray");
                 {
                     var dt0 = new DateTime(6, 7, 8, 9, 10, 11);
                     var dt1 = new DateTime(1, 2, 3, 4, 5, 6);
@@ -664,44 +700,20 @@ namespace DebuggerTests
                         TValueType("System.DateTime", dt1.ToString()),
                     };
 
-                    await CheckProps(arr_elems, exp_elems, "ptd.DTArray");
-                }
-            });
+                    await CheckProps(arr_elems, exp_elems, $"{local_name}.DTArray");
 
-        [Theory]
-        [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "MoveNext", 38, 12)]
-        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "PropertyGettersTest", 30, 12)]
-        public async Task PropertyGettersOnStructsTest(string eval_fn, string method_name, int line, int col) => await CheckInspectLocalsAtBreakpointSite(
-            "dotnet://debugger-test.dll/debugger-cfo-test.cs", line, col,
-            method_name,
-            $"window.setTimeout(function() {{ {eval_fn} }}, 1);",
-            wait_for_event_fn : async(pause_location) =>
-            {
-                var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
-                await CheckProps(frame_locals, new
-                {
-                    ptd = TObject("DebuggerTests.ClassWithProperties"),
-                        swp = TObject("DebuggerTests.StructWithProperties"),
-                }, "locals#0");
-
-                var swp = GetAndAssertObjectWithName(frame_locals, "swp");
-
-                var swp_props = await GetProperties(swp?["value"] ? ["objectId"]?.Value<string>());
-                await CheckProps(swp_props, new
-                {
-                    Int = TSymbol("int { get; }"),
-                        String = TSymbol("string { get; }"),
-                        DT = TSymbol("System.DateTime { get; }"),
-                        IntArray = TSymbol("int[] { get; }"),
-                        DTArray = TSymbol("System.DateTime[] { get; }")
-                }, "swp");
+                    res = await InvokeGetter(arr_elems[0], "Date");
+                    await CheckDateTimeValue(res.Value["result"], dt0.Date);
+                }
             });
 
         [Theory]
-        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, false)]
+        [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 38, 12, true)]
+        [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 38, 12, false)]
         [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, true)]
-        [InlineData("invoke_getters_js_test ();", "/other.js", 29, 1, false)]
-        [InlineData("invoke_getters_js_test ();", "/other.js", 29, 1, true)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, false)]
+        [InlineData("invoke_getters_js_test ();", "/other.js", 30, 1, false)]
+        [InlineData("invoke_getters_js_test ();", "/other.js", 30, 1, true)]
         public async Task CheckAccessorsOnObjectsWithCFO(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
         {
             await RunCallFunctionOn(
@@ -754,14 +766,126 @@ namespace DebuggerTests
             }
         }
 
-        async Task<Result> InvokeGetter(JToken obj, string fn, object arguments) => await ctx.cli.SendCommand(
-            "Runtime.callFunctionOn",
-            JObject.FromObject(new
+        public static TheoryData<string, string, int, int, bool> NegativeTestsData(bool use_cfo = false) => new TheoryData<string, string, int, int, bool>
+        { { "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:MethodForNegativeTests', null);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 45, 12, use_cfo },
+            { "negative_cfo_test ();", "/other.js", 62, 1, use_cfo }
+        };
+
+        [Theory]
+        [MemberData(nameof(NegativeTestsData), false)]
+        public async Task RunOnInvalidCfoId(string eval_fn, string bp_loc, int line, int col, bool use_cfo) => await RunCallFunctionOn(
+            eval_fn, "function() { return this; }", "ptd",
+            bp_loc, line, col,
+            test_fn : async(cfo_result) =>
+            {
+                var ptd_id = cfo_result.Value?["result"] ? ["objectId"]?.Value<string>();
+
+                var cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = "function () { return 0; }",
+                        objectId = ptd_id + "_invalid"
+                });
+
+                var res = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+                Assert.True(res.IsErr);
+            });
+
+        [Theory]
+        [MemberData(nameof(NegativeTestsData), false)]
+        public async Task RunOnInvalidThirdSegmentOfObjectId(string eval_fn, string bp_loc, int line, int col, bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                await SetBreakpoint(bp_loc, line, col);
+
+                // callFunctionOn
+                var eval_expr = $"window.setTimeout(function() {{ {eval_fn} }}, 1);";
+                var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token);
+                var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value<string>());
+                var ptd = GetAndAssertObjectWithName(frame_locals, "ptd");
+                var ptd_id = ptd["value"]["objectId"].Value<string>();
+
+                var cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = "function () { return 0; }",
+                        objectId = ptd_id + "_invalid"
+                });
+
+                var res = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+                Assert.True(res.IsErr);
+            });
+        }
+
+        [Theory]
+        [MemberData(nameof(NegativeTestsData), false)]
+        [MemberData(nameof(NegativeTestsData), true)]
+        public async Task InvalidPropertyGetters(string eval_fn, string bp_loc, int line, int col, bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                await SetBreakpoint(bp_loc, line, col);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+                // callFunctionOn
+                var eval_expr = $"window.setTimeout(function() {{ {eval_fn} }}, 1);";
+                await SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }));
+                var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value<string>());
+                var ptd = GetAndAssertObjectWithName(frame_locals, "ptd");
+                var ptd_id = ptd["value"]["objectId"].Value<string>();
+
+                var invalid_args = new object[] { "NonExistant", String.Empty, null, 12310 };
+                foreach (var invalid_arg in invalid_args)
+                {
+                    var getter_res = await InvokeGetter(JObject.FromObject(new { value = new { objectId = ptd_id } }), invalid_arg);
+                    AssertEqual("undefined", getter_res.Value["result"] ? ["type"]?.ToString(), $"Expected to get undefined result for non-existant accessor - {invalid_arg}");
+                }
+            });
+        }
+
+        [Theory]
+        [MemberData(nameof(NegativeTestsData), false)]
+        public async Task ReturnNullFromCFO(string eval_fn, string bp_loc, int line, int col, bool use_cfo) => await RunCallFunctionOn(
+            eval_fn, "function() { return this; }", "ptd",
+            bp_loc, line, col,
+            test_fn : async(result) =>
             {
-                functionDeclaration = fn,
-                    objectId = obj["value"] ? ["objectId"]?.Value<string>(),
-                    arguments = new [] { new { value = arguments } }
-            }), ctx.token);
+                var is_js = bp_loc.EndsWith(".js");
+                var ptd = JObject.FromObject(new { value = new { objectId = result.Value?["result"] ? ["objectId"]?.Value<string>() } });
+
+                var null_value_json = JObject.Parse("{ 'type': 'object', 'subtype': 'null', 'value': null }");
+                foreach (var returnByValue in new bool?[] { null, false, true })
+                {
+                    var res = await InvokeGetter(ptd, "StringField", returnByValue : returnByValue);
+                    if (is_js)
+                    {
+                        // In js case, it doesn't know the className, so the result looks slightly different
+                        Assert.True(
+                            JObject.DeepEquals(res.Value["result"], null_value_json),
+                            $"[StringField#returnByValue = {returnByValue}] Json didn't match. Actual: {res.Value ["result"]} vs {null_value_json}");
+                    }
+                    else
+                    {
+                        await CheckValue(res.Value["result"], TString(null), "StringField");
+                    }
+                }
+            });
 
         /*
          * 1. runs `Runtime.callFunctionOn` on the objectId,
index e121870..2a62a67 100644 (file)
@@ -534,12 +534,10 @@ namespace DebuggerTests
                 var complex = GetAndAssertObjectWithName(locals, "complex");
 
                 // try to deref the non-pointer object, as a pointer
-                var props = await GetProperties(complex["value"]["objectId"].Value<string>().Replace(":object:", ":pointer:"));
-                Assert.Empty(props.Values());
+                await GetProperties(complex["value"]["objectId"].Value<string>().Replace(":object:", ":pointer:"), expect_ok: false);
 
                 // try to deref an invalid pointer id
-                props = await GetProperties("dotnet:pointer:123897");
-                Assert.Empty(props.Values());
+                await GetProperties("dotnet:pointer:123897", expect_ok: false);
             });
 
         async Task<JToken[]> CheckArrayElements(JToken array, JToken[] exp_elems)
index cac5e73..ac85709 100644 (file)
@@ -10,6 +10,7 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 using Microsoft.WebAssembly.Diagnostics;
 using Xunit;
@@ -301,6 +302,7 @@ namespace DebuggerTests
                 var val = l["value"];
                 Assert.Equal("number", val["type"]?.Value<string>());
                 Assert.Equal(value, val["value"].Value<T>());
+                Assert.Equal(value.ToString(), val["description"].Value<T>().ToString());
                 return;
             }
             Assert.True(false, $"Could not find variable '{name}'");
@@ -308,32 +310,14 @@ namespace DebuggerTests
 
         internal void CheckString(JToken locals, string name, string value)
         {
-            foreach (var l in locals)
-            {
-                if (name != l["name"]?.Value<string>())
-                    continue;
-                var val = l["value"];
-                if (value == null)
-                {
-                    Assert.Equal("object", val["type"]?.Value<string>());
-                    Assert.Equal("null", val["subtype"]?.Value<string>());
-                }
-                else
-                {
-                    Assert.Equal("string", val["type"]?.Value<string>());
-                    Assert.Equal(value, val["value"]?.Value<string>());
-                }
-                return;
-            }
-            Assert.True(false, $"Could not find variable '{name}'");
+            var l = GetAndAssertObjectWithName(locals, name);
+            CheckValue(l["value"], TString(value), name).Wait();
         }
 
         internal JToken CheckSymbol(JToken locals, string name, string value)
         {
             var l = GetAndAssertObjectWithName(locals, name);
-            var val = l["value"];
-            Assert.Equal("symbol", val["type"]?.Value<string>());
-            Assert.Equal(value, val["value"]?.Value<string>());
+            CheckValue(l["value"], TSymbol(value), name).Wait();
             return l;
         }
 
@@ -341,14 +325,8 @@ namespace DebuggerTests
         {
             var l = GetAndAssertObjectWithName(locals, name);
             var val = l["value"];
-            Assert.Equal("object", val["type"]?.Value<string>());
+            CheckValue(val, TObject(class_name, is_null: is_null), name).Wait();
             Assert.True(val["isValueType"] == null || !val["isValueType"].Value<bool>());
-            Assert.Equal(class_name, val["className"]?.Value<string>());
-
-            var has_null_subtype = val["subtype"] != null && val["subtype"]?.Value<string>() == "null";
-            Assert.Equal(is_null, has_null_subtype);
-            if (subtype != null)
-                Assert.Equal(subtype, val["subtype"]?.Value<string>());
 
             return l;
         }
@@ -368,31 +346,34 @@ namespace DebuggerTests
 
         internal async Task CheckDateTimeValue(JToken value, DateTime expected)
         {
-            AssertEqual("System.DateTime", value["className"]?.Value<string>(), "className");
-            AssertEqual(expected.ToString(), value["description"]?.Value<string>(), "description");
-
-            var members = await GetProperties(value["objectId"]?.Value<string>());
+            await CheckDateTimeMembers(value, expected);
 
-            // not checking everything
-            CheckNumber(members, "Year", expected.Year);
-            CheckNumber(members, "Month", expected.Month);
-            CheckNumber(members, "Day", expected.Day);
-            CheckNumber(members, "Hour", expected.Hour);
-            CheckNumber(members, "Minute", expected.Minute);
-            CheckNumber(members, "Second", expected.Second);
+            var res = await InvokeGetter(JObject.FromObject(new { value = value }), "Date");
+            await CheckDateTimeMembers(res.Value["result"], expected.Date);
 
             // FIXME: check some float properties too
+
+            async Task CheckDateTimeMembers(JToken v, DateTime exp_dt)
+            {
+                AssertEqual("System.DateTime", v["className"]?.Value<string>(), "className");
+                AssertEqual(exp_dt.ToString(), v["description"]?.Value<string>(), "description");
+
+                var members = await GetProperties(v["objectId"]?.Value<string>());
+
+                // not checking everything
+                CheckNumber(members, "Year", exp_dt.Year);
+                CheckNumber(members, "Month", exp_dt.Month);
+                CheckNumber(members, "Day", exp_dt.Day);
+                CheckNumber(members, "Hour", exp_dt.Hour);
+                CheckNumber(members, "Minute", exp_dt.Minute);
+                CheckNumber(members, "Second", exp_dt.Second);
+            }
         }
 
         internal JToken CheckBool(JToken locals, string name, bool expected)
         {
             var l = GetAndAssertObjectWithName(locals, name);
-            var val = l["value"];
-            Assert.Equal("boolean", val["type"]?.Value<string>());
-            if (val["value"] == null)
-                Assert.True(false, "expected bool value not found for variable named {name}");
-            Assert.Equal(expected, val["value"]?.Value<bool>());
-
+            CheckValue(l["value"], TBool(expected), name).Wait();
             return l;
         }
 
@@ -405,41 +386,21 @@ namespace DebuggerTests
         internal JToken CheckValueType(JToken locals, string name, string class_name)
         {
             var l = GetAndAssertObjectWithName(locals, name);
-            var val = l["value"];
-            Assert.Equal("object", val["type"]?.Value<string>());
-            Assert.True(val["isValueType"] != null && val["isValueType"].Value<bool>());
-            Assert.Equal(class_name, val["className"]?.Value<string>());
+            CheckValue(l["value"], TValueType(class_name), name).Wait();
             return l;
         }
 
         internal JToken CheckEnum(JToken locals, string name, string class_name, string descr)
         {
             var l = GetAndAssertObjectWithName(locals, name);
-            var val = l["value"];
-            Assert.Equal("object", val["type"]?.Value<string>());
-            Assert.True(val["isEnum"] != null && val["isEnum"].Value<bool>());
-            Assert.Equal(class_name, val["className"]?.Value<string>());
-            Assert.Equal(descr, val["description"]?.Value<string>());
+            CheckValue(l["value"], TEnum(class_name, descr), name).Wait();
             return l;
         }
 
-        internal void CheckArray(JToken locals, string name, string class_name)
-        {
-            foreach (var l in locals)
-            {
-                if (name != l["name"]?.Value<string>())
-                    continue;
-
-                var val = l["value"];
-                Assert.Equal("object", val["type"]?.Value<string>());
-                Assert.Equal("array", val["subtype"]?.Value<string>());
-                Assert.Equal(class_name, val["className"]?.Value<string>());
-
-                //FIXME: elements?
-                return;
-            }
-            Assert.True(false, $"Could not find variable '{name}'");
-        }
+        internal void CheckArray(JToken locals, string name, string class_name, int length)
+           => CheckValue(
+                GetAndAssertObjectWithName(locals, name)["value"],
+                TArray(class_name, length), name).Wait();
 
         internal JToken GetAndAssertObjectWithName(JToken obj, string name)
         {
@@ -482,6 +443,23 @@ namespace DebuggerTests
             return wait_res;
         }
 
+        internal async Task<Result> InvokeGetter(JToken obj, object arguments, string fn = "function(e){return this[e]}", bool expect_ok = true, bool? returnByValue = null)
+        {
+            var req = JObject.FromObject(new
+            {
+                functionDeclaration = fn,
+                objectId            = obj["value"]?["objectId"]?.Value<string>(),
+                arguments           = new[] { new { value = arguments } }
+            });
+            if (returnByValue != null)
+                req["returnByValue"] = returnByValue.Value;
+
+            var res = await ctx.cli.SendCommand("Runtime.callFunctionOn", req, ctx.token);
+            Assert.True(expect_ok == res.IsOk, $"InvokeGetter failed for {req} with {res}");
+
+            return res;
+        }
+
         internal async Task<JObject> StepAndCheck(StepKind kind, string script_loc, int line, int column, string function_name,
             Func<JObject, Task> wait_for_event_fn = null, Action<JToken> locals_fn = null, int times = 1)
         {
@@ -796,7 +774,7 @@ namespace DebuggerTests
         }
 
         /* @fn_args is for use with `Runtime.callFunctionOn` only */
-        internal async Task<JToken> GetProperties(string id, JToken fn_args = null)
+        internal async Task<JToken> GetProperties(string id, JToken fn_args = null, bool expect_ok = true)
         {
             if (ctx.UseCallFunctionOnBeforeGetProperties && !id.StartsWith("dotnet:scope:"))
             {
@@ -810,7 +788,9 @@ namespace DebuggerTests
                     cfo_args["arguments"] = fn_args;
 
                 var result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
-                AssertEqual(true, result.IsOk, $"Runtime.getProperties failed for {cfo_args.ToString ()}, with Result: {result}");
+                AssertEqual(expect_ok, result.IsOk, $"Runtime.getProperties returned {result.IsOk} instead of {expect_ok}, for {cfo_args.ToString ()}, with Result: {result}");
+                if (!result.IsOk)
+                    return null;
                 id = result.Value["result"] ? ["objectId"]?.Value<string>();
             }
 
@@ -820,8 +800,9 @@ namespace DebuggerTests
             });
 
             var frame_props = await ctx.cli.SendCommand("Runtime.getProperties", get_prop_req, ctx.token);
+            AssertEqual(expect_ok, frame_props.IsOk, $"Runtime.getProperties returned {frame_props.IsOk} instead of {expect_ok}, for {get_prop_req}, with Result: {frame_props}");
             if (!frame_props.IsOk)
-                Assert.True(false, $"Runtime.getProperties failed for {get_prop_req.ToString ()}, with Result: {frame_props}");
+                return null;
 
             var locals = frame_props.Value["result"];
             // FIXME: Should be done when generating the list in library_mono.js, but not sure yet
@@ -927,11 +908,14 @@ namespace DebuggerTests
         internal static JObject TString(string value) =>
             value == null ?
             TObject("string", is_null : true) :
-            JObject.FromObject(new { type = "string", value = @value, description = @value });
+            JObject.FromObject(new { type = "string", value = @value });
 
         internal static JObject TNumber(int value) =>
             JObject.FromObject(new { type = "number", value = @value.ToString(), description = value.ToString() });
 
+        internal static JObject TNumber(uint value) =>
+            JObject.FromObject(new { type = "number", value = @value.ToString(), description = value.ToString() });
+
         internal static JObject TValueType(string className, string description = null, object members = null) =>
             JObject.FromObject(new { type = "object", isValueType = true, className = className, description = description ?? className });
 
@@ -987,6 +971,65 @@ namespace DebuggerTests
         }
     }
 
+    class DotnetObjectId
+    {
+        public string Scheme { get; }
+        public string Value { get; }
+
+        JObject value_json;
+        public JObject ValueAsJson
+        {
+            get
+            {
+                if (value_json == null)
+                {
+                    try
+                    {
+                        value_json = JObject.Parse(Value);
+                    }
+                    catch (JsonReaderException) { }
+                }
+
+                return value_json;
+            }
+        }
+
+        public static bool TryParse(JToken jToken, out DotnetObjectId objectId) => TryParse(jToken?.Value<string>(), out objectId);
+
+        public static bool TryParse(string id, out DotnetObjectId objectId)
+        {
+            objectId = null;
+            if (id == null)
+            {
+                return false;
+            }
+
+            if (!id.StartsWith("dotnet:"))
+            {
+                return false;
+            }
+
+            var parts = id.Split(":", 3);
+
+            if (parts.Length < 3)
+            {
+                return false;
+            }
+
+            objectId = new DotnetObjectId(parts[1], parts[2]);
+
+            return true;
+        }
+
+        public DotnetObjectId(string scheme, string value)
+        {
+            Scheme = scheme;
+            Value = value;
+        }
+
+        public override string ToString() => $"dotnet:{Scheme}:{Value}";
+    }
+
     enum StepKind
     {
         Into,
index 03964dd..5ba31d8 100644 (file)
@@ -372,14 +372,14 @@ namespace DebuggerTests
                     CheckObject(locals, "list", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>");
                     CheckObject(locals, "list_null", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>", is_null : true);
 
-                    CheckArray(locals, "list_arr", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]");
+                    CheckArray(locals, "list_arr", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]", 1);
                     CheckObject(locals, "list_arr_null", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]", is_null : true);
 
                     // Unused locals
                     CheckObject(locals, "list_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>");
                     CheckObject(locals, "list_null_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>", is_null : true);
 
-                    CheckObject(locals, "list_arr_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]");
+                    CheckArray(locals, "list_arr_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]", 1);
                     CheckObject(locals, "list_arr_null_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]", is_null : true);
                 }
             );
@@ -638,9 +638,11 @@ namespace DebuggerTests
                 CheckValueType(this_props, "SimpleStructProperty", "Math.SimpleStruct");
 
                 var ss_props = await GetObjectOnLocals(this_props, "SimpleStructProperty");
-                Assert.Equal(2, ss_props.Count());
-                CheckValueType(ss_props, "dt", "System.DateTime");
-                CheckValueType(ss_props, "gs", "Math.GenericStruct<System.DateTime>");
+                var dt = new DateTime(2020, 1, 2, 3, 4, 5);
+                await CheckProps(ss_props, new {
+                    dt = TValueType("System.DateTime", dt.ToString()),
+                    gs = TValueType("Math.GenericStruct<System.DateTime>")
+                }, "ss_props");
 
                 await CheckDateTime(ss_props, "dt", new DateTime(2020, 1, 2, 3, 4, 5));
 
@@ -829,7 +831,7 @@ namespace DebuggerTests
                         CheckObject(locals, "this", "Math.NestedInMath");
                         //FIXME: check fields
                         CheckValueType(locals, "ss", "Math.SimpleStruct");
-                        CheckArray(locals, "ss_arr", "Math.SimpleStruct[]");
+                        CheckArray(locals, "ss_arr", "Math.SimpleStruct[]", 0);
                         // TODO: struct fields
                     }
                 );
@@ -863,29 +865,40 @@ namespace DebuggerTests
 
                 var pause_location = await EvaluateAndCheck(
                     "window.setTimeout(function() { invoke_method_with_structs(); }, 1);",
-                    debugger_test_loc, 22, 8, "MethodWithLocalStructs",
-                    locals_fn: (locals) =>
-                    {
-                        Assert.Equal(3, locals.Count());
+                    debugger_test_loc, 22, 8, "MethodWithLocalStructs");
 
-                        CheckValueType(locals, "ss_local", "DebuggerTests.ValueTypesTest.SimpleStruct");
-                        CheckValueType(locals, "gs_local", "DebuggerTests.ValueTypesTest.GenericStruct<DebuggerTests.ValueTypesTest>");
-                        CheckObject(locals, "vt_local", "DebuggerTests.ValueTypesTest");
-                    }
-                );
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckProps(locals, new
+                {
+                    ss_local = TValueType("DebuggerTests.ValueTypesTest.SimpleStruct"),
+                    gs_local = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<DebuggerTests.ValueTypesTest>"),
+                    vt_local = TObject("DebuggerTests.ValueTypesTest")
+                }, "locals");
 
                 var dt = new DateTime(2021, 2, 3, 4, 6, 7);
+                var vt_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "vt_local");
+                Assert.Equal(5, vt_local_props.Count());
+
+                CheckString(vt_local_props, "StringField", "string#0");
+                CheckValueType(vt_local_props, "SimpleStructField", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                CheckValueType(vt_local_props, "SimpleStructProperty", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                await CheckDateTime(vt_local_props, "DT", new DateTime(2020, 1, 2, 3, 4, 5));
+                CheckEnum(vt_local_props, "RGB", "DebuggerTests.RGB", "Blue");
+
                 // Check ss_local's properties
                 var ss_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local");
                 await CheckProps(ss_local_props, new
                 {
+                    V          = TGetter("V"),
                     str_member = TString("set in MethodWithLocalStructs#SimpleStruct#str_member"),
-                        dt = TValueType("System.DateTime", dt.ToString()),
-                        gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
-                        Kind = TEnum("System.DateTimeKind", "Utc")
+                    dt         = TValueType("System.DateTime", dt.ToString()),
+                    gs         = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                    Kind       = TEnum("System.DateTimeKind", "Utc")
                 }, "ss_local");
 
                 {
+                    var gres = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_local"), "V");
+                    await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + 2), $"ss_local#V");
                     // Check ss_local.dt
                     await CheckDateTime(ss_local_props, "dt", dt);
 
@@ -905,43 +918,30 @@ namespace DebuggerTests
                 }, "gs_local");
 
                 // Check vt_local's properties
-                var vt_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "vt_local");
-                Assert.Equal(5, vt_local_props.Count());
 
-                CheckString(vt_local_props, "StringField", "string#0");
-                CheckValueType(vt_local_props, "SimpleStructField", "DebuggerTests.ValueTypesTest.SimpleStruct");
-                CheckValueType(vt_local_props, "SimpleStructProperty", "DebuggerTests.ValueTypesTest.SimpleStruct");
-                await CheckDateTime(vt_local_props, "DT", new DateTime(2020, 1, 2, 3, 4, 5));
-                CheckEnum(vt_local_props, "RGB", "DebuggerTests.RGB", "Blue");
+                var exp = new[]
+                {
+                    ("SimpleStructProperty", 2, "Utc"),
+                    ("SimpleStructField", 5, "Local")
+                };
 
+                foreach (var (name, bias, dt_kind) in exp)
                 {
-                    // SimpleStructProperty
-                    dt = new DateTime(2022, 3, 4, 5, 7, 8);
-                    var ssp_props = await CompareObjectPropertiesFor(vt_local_props, "SimpleStructProperty",
+                    dt = new DateTime(2020 + bias, 1 + bias, 2 + bias, 3 + bias, 5 + bias, 6 + bias);
+                    var ssp_props = await CompareObjectPropertiesFor(vt_local_props, name,
                         new
                         {
-                            str_member = TString("SimpleStructProperty#string#0#SimpleStruct#str_member"),
-                                dt = TValueType("System.DateTime", dt.ToString()),
-                                gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
-                                Kind = TEnum("System.DateTimeKind", "Utc")
+                            V          = TGetter("V"),
+                            str_member = TString($"{name}#string#0#SimpleStruct#str_member"),
+                            dt         = TValueType("System.DateTime", dt.ToString()),
+                            gs         = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                            Kind       = TEnum("System.DateTimeKind", dt_kind)
                         },
-                        label: "vt_local_props.SimpleStructProperty");
+                        label: $"vt_local_props.{name}");
 
                     await CheckDateTime(ssp_props, "dt", dt);
-
-                    // SimpleStructField
-                    dt = new DateTime(2025, 6, 7, 8, 10, 11);
-                    var ssf_props = await CompareObjectPropertiesFor(vt_local_props, "SimpleStructField",
-                        new
-                        {
-                            str_member = TString("SimpleStructField#string#0#SimpleStruct#str_member"),
-                                dt = TValueType("System.DateTime", dt.ToString()),
-                                gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
-                                Kind = TEnum("System.DateTimeKind", "Local")
-                        },
-                        label: "vt_local_props.SimpleStructField");
-
-                    await CheckDateTime(ssf_props, "dt", dt);
+                    var gres = await InvokeGetter(GetAndAssertObjectWithName(vt_local_props, name), "V");
+                    await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + (uint) dt.Month), $"{name}#V");
                 }
 
                 // FIXME: check ss_local.gs.List's members
@@ -968,20 +968,19 @@ namespace DebuggerTests
 
                 var pause_location = await EvaluateAndCheck(
                     "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:TestStructsAsMethodArgs'); }, 1);",
-                    debugger_test_loc, 34, 12, "MethodWithStructArgs",
-                    locals_fn: (locals) =>
-                    {
-                        Assert.Equal(3, locals.Count());
-
-                        CheckString(locals, "label", "TestStructsAsMethodArgs#label");
-                        CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct");
-                        CheckNumber(locals, "x", 3);
-                    }
-                );
+                    debugger_test_loc, 34, 12, "MethodWithStructArgs");
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                {
+                    Assert.Equal(3, locals.Count());
+                    CheckString(locals, "label", "TestStructsAsMethodArgs#label");
+                    CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                    CheckNumber(locals, "x", 3);
+                }
 
                 var dt = new DateTime(2025, 6, 7, 8, 10, 11);
                 var ss_local_as_ss_arg = new
                 {
+                    V = TGetter("V"),
                     str_member = TString("ss_local#SimpleStruct#string#0#SimpleStruct#str_member"),
                     dt = TValueType("System.DateTime", dt.ToString()),
                     gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
@@ -998,6 +997,9 @@ namespace DebuggerTests
                 var ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_arg");
                 await CheckProps(ss_arg_props, ss_local_as_ss_arg, "ss_arg");
 
+                var res = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_arg"), "V");
+                await CheckValue(res.Value["result"], TNumber(0xDEADBEEF + (uint) dt.Month), "ss_arg#V");
+
                 {
                     // Check ss_local.dt
                     await CheckDateTime(ss_arg_props, "dt", dt);
@@ -1007,19 +1009,19 @@ namespace DebuggerTests
                 }
 
                 pause_location = await StepAndCheck(StepKind.Over, debugger_test_loc, 38, 8, "MethodWithStructArgs", times : 4,
-                    locals_fn: (locals) =>
-                    {
-                        Assert.Equal(3, locals.Count());
-
-                        CheckString(locals, "label", "TestStructsAsMethodArgs#label");
-                        CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct");
-                        CheckNumber(locals, "x", 3);
+                    locals_fn: (l) => { /* non-null to make sure that locals get fetched */ });
+                locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                {
+                    Assert.Equal(3, locals.Count());
 
-                    }
-                );
+                    CheckString(locals, "label", "TestStructsAsMethodArgs#label");
+                    CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                    CheckNumber(locals, "x", 3);
+                }
 
                 var ss_arg_updated = new
                 {
+                    V = TGetter("V"),
                     str_member = TString("ValueTypesTest#MethodWithStructArgs#updated#ss_arg#str_member"),
                     dt = TValueType("System.DateTime", dt.ToString()),
                     gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
@@ -1027,7 +1029,10 @@ namespace DebuggerTests
                 };
 
                 ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_arg");
-                await CheckProps(ss_arg_props, ss_arg_updated, "ss_ar");
+                await CheckProps(ss_arg_props, ss_arg_updated, "ss_arg");
+
+                res = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_arg"), "V");
+                await CheckValue(res.Value["result"], TNumber(0xDEADBEEF + (uint) dt.Month), "ss_arg#V");
 
                 {
                     // Check ss_local.gs
@@ -1059,7 +1064,7 @@ namespace DebuggerTests
 
                 pause_location = await StepAndCheck(StepKind.Over, debugger_test_loc, 28, 12, "TestStructsAsMethodArgs",
                     times : 2, locals_fn: (l) => { /* non-null to make sure that locals get fetched */ });
-                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
                 await CheckProps(locals, new
                     {
                         ss_local = TValueType("DebuggerTests.ValueTypesTest.SimpleStruct"),
@@ -1092,7 +1097,7 @@ namespace DebuggerTests
                 ctx = new DebugTestContext(cli, insp, token, scripts);
                 var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
 
-                var lines = new [] { 202, 205 };
+                var lines = new [] { 203, 206 };
                 await SetBreakpoint(debugger_test_loc, lines[0], 12);
                 await SetBreakpoint(debugger_test_loc, lines[1], 12);
 
@@ -1100,16 +1105,14 @@ namespace DebuggerTests
                     "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:MethodUpdatingValueTypeMembers'); }, 1);",
                     debugger_test_loc, lines[0], 12, "MethodUpdatingValueTypeMembers");
 
-                var dt = new DateTime(1, 2, 3, 4, 5, 6);
-                await CheckLocals(pause_location, dt);
+                await CheckLocals(pause_location, new DateTime(1, 2, 3, 4, 5, 6), new DateTime(4, 5, 6, 7, 8, 9));
 
                 // Resume
-                dt = new DateTime(9, 8, 7, 6, 5, 4);
                 pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", debugger_test_loc, lines[1], 12, "MethodUpdatingValueTypeMembers");
-                await CheckLocals(pause_location, dt);
+                await CheckLocals(pause_location, new DateTime(9, 8, 7, 6, 5, 4), new DateTime(5, 1, 3, 7, 9, 10));
             });
 
-            async Task CheckLocals(JToken pause_location, DateTime dt)
+            async Task CheckLocals(JToken pause_location, DateTime obj_dt, DateTime vt_dt)
             {
                 var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
                 await CheckProps(locals, new
@@ -1122,20 +1125,20 @@ namespace DebuggerTests
                 {
                     await CheckProps(obj_props, new
                     {
-                        DT = TValueType("System.DateTime", dt.ToString())
+                        DT = TValueType("System.DateTime", obj_dt.ToString())
                     }, "locals#obj.DT", num_fields : 5);
 
-                    await CheckDateTime(obj_props, "DT", dt);
+                    await CheckDateTime(obj_props, "DT", obj_dt);
                 }
 
-                var vt_props = await GetObjectOnLocals(locals, "obj");
+                var vt_props = await GetObjectOnLocals(locals, "vt");
                 {
                     await CheckProps(vt_props, new
                     {
-                        DT = TValueType("System.DateTime", dt.ToString())
+                        DT = TValueType("System.DateTime", vt_dt.ToString())
                     }, "locals#obj.DT", num_fields : 5);
 
-                    await CheckDateTime(vt_props, "DT", dt);
+                    await CheckDateTime(vt_props, "DT", vt_dt);
                 }
             }
         }
@@ -1153,7 +1156,7 @@ namespace DebuggerTests
                 ctx = new DebugTestContext(cli, insp, token, scripts);
                 var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
 
-                var lines = new [] { 211, 213 };
+                var lines = new [] { 212, 214 };
                 await SetBreakpoint(debugger_test_loc, lines[0], 12);
                 await SetBreakpoint(debugger_test_loc, lines[1], 12);
 
@@ -1186,7 +1189,7 @@ namespace DebuggerTests
                 ctx = new DebugTestContext(cli, insp, token, scripts);
                 var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
 
-                var lines = new [] { 222, 224 };
+                var lines = new [] { 223, 225 };
                 await SetBreakpoint(debugger_test_loc, lines[0], 12);
                 await SetBreakpoint(debugger_test_loc, lines[1], 12);
 
@@ -1259,13 +1262,17 @@ namespace DebuggerTests
                 var ss_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local");
                 await CheckProps(ss_local_props, new
                 {
+                    V          = TGetter("V"),
                     str_member = TString("set in MethodWithLocalStructsStaticAsync#SimpleStruct#str_member"),
-                        dt = TValueType("System.DateTime", dt.ToString()),
-                        gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
-                        Kind = TEnum("System.DateTimeKind", "Utc")
+                    dt         = TValueType("System.DateTime", dt.ToString()),
+                    gs         = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                    Kind       = TEnum("System.DateTimeKind", "Utc")
                 }, "ss_local");
 
                 {
+                    var gres = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_local"), "V");
+                    await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + 2), $"ss_local#V");
+
                     // Check ss_local.dt
                     await CheckDateTime(ss_local_props, "dt", dt);
 
@@ -1294,10 +1301,10 @@ namespace DebuggerTests
         }
 
         [Theory]
-        [InlineData(134, 12, "MethodWithLocalsForToStringTest", false, false)]
-        [InlineData(144, 12, "MethodWithArgumentsForToStringTest", true, false)]
-        [InlineData(189, 12, "MethodWithArgumentsForToStringTestAsync", true, true)]
-        [InlineData(179, 12, "MethodWithArgumentsForToStringTestAsync", false, true)]
+        [InlineData(135, 12, "MethodWithLocalsForToStringTest", false, false)]
+        [InlineData(145, 12, "MethodWithArgumentsForToStringTest", true, false)]
+        [InlineData(190, 12, "MethodWithArgumentsForToStringTestAsync", true, true)]
+        [InlineData(180, 12, "MethodWithArgumentsForToStringTestAsync", false, true)]
         public async Task InspectLocalsForToStringDescriptions(int line, int col, string method_name, bool call_other, bool invoke_async)
         {
             var insp = new Inspector();
@@ -1478,6 +1485,43 @@ namespace DebuggerTests
             });
         }
 
+        [Fact]
+        public async Task InvalidValueTypeData()
+        {
+            await CheckInspectLocalsAtBreakpointSite(
+                "dotnet://debugger-test.dll/debugger-test.cs", 85, 8,
+                "OuterMethod",
+                "window.setTimeout(function() { invoke_static_method ('[debugger-test] Math:OuterMethod'); })",
+                wait_for_event_fn : async(pause_location) =>
+                {
+                    var new_id = await CreateNewId(@"MONO._new_or_add_id_props ({ scheme: 'valuetype', idArgs: { containerId: 1 }, props: { klass: 3, value64: 4 }});");
+                    await _invoke_getter(new_id, "NonExistant", expect_ok : false);
+
+                    new_id = await CreateNewId(@"MONO._new_or_add_id_props ({ scheme: 'valuetype', idArgs: { containerId: 1 }, props: { klass: 3 }});");
+                    await _invoke_getter(new_id, "NonExistant", expect_ok : false);
+
+                    new_id = await CreateNewId(@"MONO._new_or_add_id_props ({ scheme: 'valuetype', idArgs: { containerId: 1 }, props: { klass: 3, value64: 'AA' }});");
+                    await _invoke_getter(new_id, "NonExistant", expect_ok : false);
+                });
+
+            async Task<string> CreateNewId(string expr)
+            {
+                var res = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = expr }), ctx.token);
+                Assert.True(res.IsOk, "Expected Runtime.evaluate to succeed");
+                AssertEqual("string", res.Value["result"] ? ["type"]?.Value<string>(), "Expected Runtime.evaluate to return a string type result");
+                return res.Value["result"] ? ["value"]?.Value<string>();
+            }
+
+            async Task<Result> _invoke_getter(string obj_id, string property_name, bool expect_ok)
+            {
+                var expr = $"MONO._invoke_getter ('{obj_id}', '{property_name}')";
+                var res = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = expr }), ctx.token);
+                AssertEqual(expect_ok, res.IsOk, "Runtime.evaluate result not as expected for {expr}");
+
+                return res;
+            }
+        }
+
         //TODO add tests covering basic stepping behavior as step in/out/over
     }
-}
\ No newline at end of file
+}
index ad3b12c..6e88564 100644 (file)
@@ -26,24 +26,32 @@ namespace DebuggerTests
 
         public static void PropertyGettersTest()
         {
-            var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9) };
-            var swp = new StructWithProperties();
+            var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9), V = 0xDEADBEEF };
+            var swp = new StructWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9), V = 0xDEADBEEF };
             System.Console.WriteLine("break here");
         }
 
         public static async System.Threading.Tasks.Task PropertyGettersTestAsync()
         {
-            var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9) };
-            var swp = new StructWithProperties();
+            var ptd = new ClassWithProperties { DTAutoProperty = new DateTime (4, 5, 6, 7, 8, 9), V = 0xDEADBEEF };
+            var swp = new StructWithProperties { DTAutoProperty = new DateTime (4, 5, 6, 7, 8, 9), V = 0xDEADBEEF };
             System.Console.WriteLine("break here");
             await System.Threading.Tasks.Task.CompletedTask;
         }
+
+        public static void MethodForNegativeTests (string value = null)
+        {
+            var ptd = new ClassWithProperties { StringField = value };
+            var swp = new StructWithProperties { StringField = value };
+            Console.WriteLine("break here");
+        }
     }
 
     class ClassWithProperties
     {
-        public int Int { get { return 5; } }
-        public string String { get { return "foobar"; } }
+        public uint V;
+        public uint Int { get { return V + (uint)DT.Month; } }
+        public string String { get { return $"String property, V: 0x{V:X}"; } }
         public DateTime DT { get { return new DateTime(3, 4, 5, 6, 7, 8); } }
 
         public int[] IntArray { get { return new int[] { 10, 20 }; } }
@@ -54,11 +62,14 @@ namespace DebuggerTests
 
     struct StructWithProperties
     {
-        public int Int { get { return 5; } }
-        public string String { get { return "foobar"; } }
+        public uint V;
+        public uint Int { get { return V + (uint)DT.Month; } }
+        public string String { get { return $"String property, V: 0x{V:X}"; } }
         public DateTime DT { get { return new DateTime(3, 4, 5, 6, 7, 8); } }
 
         public int[] IntArray { get { return new int[] { 10, 20 }; } }
         public DateTime[] DTArray { get { return new DateTime[] { new DateTime(6, 7, 8, 9, 10, 11), new DateTime(1, 2, 3, 4, 5, 6) }; } }
+        public DateTime DTAutoProperty { get; set; }
+        public string StringField;
     }
 }
\ No newline at end of file
index 3a4ceea..7c37287 100644 (file)
@@ -61,6 +61,7 @@ namespace DebuggerTests
 
         public struct SimpleStruct
         {
+            public uint V { get { return 0xDEADBEEF + (uint)dt.Month; } set { } }
             public string str_member;
             public DateTime dt;
             public GenericStruct<DateTime> gs;
index a1b121e..9353268 100644 (file)
@@ -50,3 +50,16 @@ function exceptions_test () {
        exception_uncaught_test ();
 }
 
+function negative_cfo_test (str_value = null) {
+       var ptd = {
+               get Int () { return 5; },
+               get String () { return "foobar"; },
+               get DT () { return "dt"; },
+               get IntArray () { return [1,2,3]; },
+               get DTArray () { return ["dt0", "dt1"]; },
+               DTAutoProperty: "dt",
+               StringField: str_value
+       };
+       console.log (`break here`);
+       return ptd;
+}
index 5e1a177..14e191e 100644 (file)
@@ -1,6 +1,15 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+/**
+ * @typedef WasmId
+ * @type {object}
+ * @property {string} idStr - full object id string
+ * @property {string} scheme - eg, object, valuetype, array ..
+ * @property {string} value - string part after `dotnet:scheme:` of the id string
+ * @property {object} o - value parsed as JSON
+ */
+
 var MonoSupportLib = {
        $MONO__postset: 'MONO.export_functions (Module);',
        $MONO: {
@@ -9,6 +18,10 @@ var MonoSupportLib = {
                _vt_stack: [],
                mono_wasm_runtime_is_ready : false,
                mono_wasm_ignore_pdb_load_errors: true,
+
+               /** @type {object.<string, object>} */
+               _id_table: {},
+
                pump_message: function () {
                        if (!this.mono_background_exec)
                                this.mono_background_exec = Module.cwrap ("mono_background_exec", null);
@@ -98,30 +111,19 @@ var MonoSupportLib = {
                },
 
                _fixup_name_value_objects: function (var_list) {
-                       var out_list = [];
-
-                       var _fixup_value = function (value) {
-                               if (value != null && value != undefined) {
-                                       var descr = value.description;
-                                       if (descr == null || descr == undefined)
-                                               value.description = '' + value.value;
-                               }
-                               return value;
-                       };
+                       let out_list = [];
 
                        var i = 0;
                        while (i < var_list.length) {
-                               var o = var_list [i];
-                               var name = o.name;
+                               let o = var_list [i];
+                               const name = o.name;
                                if (name == null || name == undefined) {
                                        i ++;
-                                       o.value = _fixup_value(o.value);
                                        out_list.push (o);
                                        continue;
                                }
 
                                if (i + 1 < var_list.length) {
-                                       _fixup_value(var_list[i + 1].value);
                                        o = Object.assign (o, var_list [i + 1]);
                                }
 
@@ -133,8 +135,8 @@ var MonoSupportLib = {
                },
 
                _filter_automatic_properties: function (props) {
-                       var names_found = {};
-                       var final_var_list = [];
+                       let names_found = {};
+                       let final_var_list = [];
 
                        for (var i in props) {
                                var p = props [i];
@@ -153,46 +155,116 @@ var MonoSupportLib = {
                        return final_var_list;
                },
 
-               // Given `dotnet:object:foo:bar`,
-               // returns [ 'dotnet', 'object', 'foo:bar']
-               _split_object_id: function (id, delimiter = ':', count = 3) {
-                       if (id === undefined || id == "")
-                               return [];
+               /** Given `dotnet:object:foo:bar`,
+                * returns { scheme:'object', value: 'foo:bar' }
+                *
+                * Given `dotnet:pointer:{ b: 3 }`
+                * returns { scheme:'object', value: '{b:3}`, o: {b:3}
+                *
+                * @param  {string} idStr
+                * @param  {boolean} [throwOnError=false]
+                *
+                * @returns {WasmId}
+                */
+               _parse_object_id: function (idStr, throwOnError = false) {
+                       if (idStr === undefined || idStr == "" || !idStr.startsWith ('dotnet:')) {
+                               if (throwOnError)
+                                       throw new Error (`Invalid id: ${idStr}`);
+
+                               return undefined;
+                       }
+
+                       const [, scheme, ...rest] = idStr.split(':');
+                       let res = {
+                               scheme,
+                               value: rest.join (':'),
+                               idStr,
+                               o: {}
+                       };
+
+                       try {
+                               res.o = JSON.parse(res.value);
+                       // eslint-disable-next-line no-empty
+                       } catch (e) {}
 
-                       if (delimiter === undefined) delimiter = ':';
-                       if (count === undefined) count = 3;
+                       return res;
+               },
+
+               /**
+                * @param  {WasmId} id
+                * @returns {object[]}
+                */
+               _get_vt_properties: function (id) {
+                       let entry = this._id_table [id.idStr];
+                       if (entry !== undefined && entry.members !== undefined)
+                               return entry.members;
+
+                       if (!isNaN (id.o.containerId))
+                               this._get_object_properties (id.o.containerId, true);
+                       else if (!isNaN (id.o.arrayId))
+                               this._get_array_values (id, Number (id.o.arrayIdx), 1, true);
+                       else
+                               throw new Error (`Invalid valuetype id (${id.idStr}). Can't get properties for it.`);
 
-                       var var_arr = id.split (delimiter);
-                       var result = var_arr.splice (0, count - 1);
+                       entry = this._get_id_props (id.idStr);
+                       if (entry !== undefined && entry.members !== undefined)
+                               return entry.members;
 
-                       if (var_arr.length > 0)
-                               result.push (var_arr.join (delimiter));
-                       return result;
+                       throw new Error (`Unknown valuetype id: ${id.idStr}`);
+               },
+
+               /**
+                *
+                * @callback GetIdArgsCallback
+                * @param {object} var
+                * @param {number} idx
+                * @returns {object}
+                */
+
+               /**
+                * @param  {object[]} vars
+                * @param  {GetIdArgsCallback} getIdArgs
+                * @returns {object}
+                */
+               _assign_vt_ids: function (vars, getIdArgs)
+               {
+                       vars.forEach ((v, i) => {
+                               // we might not have a `.value`, like in case of getters which have a `.get` instead
+                               const value = v.value;
+                               if (value === undefined || !value.isValueType)
+                                       return;
+
+                               if (value.objectId !== undefined)
+                                       throw new Error (`Bug: Trying to assign valuetype id, but the var already has one: ${v}`);
+
+                               value.objectId = this._new_or_add_id_props ({ scheme: 'valuetype', idArgs: getIdArgs (v, i), props: value._props });
+                               delete value._props;
+                       });
+
+                       return vars;
                },
 
                //
                // @var_list: [ { index: <var_id>, name: <var_name> }, .. ]
                mono_wasm_get_variables: function(scope, var_list) {
-                       if (!this.mono_wasm_get_var_info)
-                               this.mono_wasm_get_var_info = Module.cwrap ("mono_wasm_get_var_info", null, [ 'number', 'number', 'number']);
-
-                       this.var_info = [];
-                       var numBytes = var_list.length * Int32Array.BYTES_PER_ELEMENT;
-                       var ptr = Module._malloc(numBytes);
-                       var heapBytes = new Int32Array(Module.HEAP32.buffer, ptr, numBytes);
+                       const numBytes = var_list.length * Int32Array.BYTES_PER_ELEMENT;
+                       const ptr = Module._malloc(numBytes);
+                       let heapBytes = new Int32Array(Module.HEAP32.buffer, ptr, numBytes);
                        for (let i=0; i<var_list.length; i++) {
                                heapBytes[i] = var_list[i].index;
                        }
 
                        this._async_method_objectId = 0;
-                       this.mono_wasm_get_var_info (scope, heapBytes.byteOffset, var_list.length);
+                       let { res_ok, res } = this.mono_wasm_get_local_vars_info (scope, heapBytes.byteOffset, var_list.length);
                        Module._free(heapBytes.byteOffset);
-                       var res = MONO._fixup_name_value_objects (this.var_info);
+                       if (!res_ok)
+                               throw new Error (`Failed to get locals for scope ${scope}`);
 
-                       for (let i in res) {
-                               var res_name = res [i].name;
+                       if (this._async_method_objectId != 0)
+                               this._assign_vt_ids (res, v => ({ containerId: this._async_method_objectId, fieldOffset: v.fieldOffset }));
 
-                               var value = res[i].value;
+                       for (let i in res) {
+                               const res_name = res [i].name;
                                if (this._async_method_objectId != 0) {
                                        //Async methods are special in the way that local variables can be lifted to generated class fields
                                        //value of "this" comes here either
@@ -201,9 +273,6 @@ var MonoSupportLib = {
                                                // ALTHOUGH, the name wouldn't have `<>` for method args
                                                res [i].name = res_name.substring (1, res_name.indexOf ('>'));
                                        }
-
-                                       if (value.isValueType)
-                                               value.objectId = `dotnet:valuetype:${this._async_method_objectId}:${res [i].fieldOffset}`;
                                } else if (res_name === undefined && var_list [i] !== undefined) {
                                        // For non-async methods, we just have the var id, but we have the name
                                        // from the caller
@@ -212,70 +281,49 @@ var MonoSupportLib = {
                        }
 
                        this._post_process_details(res);
-                       this.var_info = []
-
                        return res;
                },
 
-
-               mono_wasm_get_object_properties: function(objId, expandValueTypes) {
-                       if (!this.mono_wasm_get_object_properties_info)
-                               this.mono_wasm_get_object_properties_info = Module.cwrap ("mono_wasm_get_object_properties", null, [ 'number', 'bool' ]);
-
-                       this.var_info = [];
-                       this.mono_wasm_get_object_properties_info (objId, expandValueTypes);
-
-                       var res = MONO._filter_automatic_properties (MONO._fixup_name_value_objects (this.var_info));
-                       for (var i = 0; i < res.length; i++) {
-                               var res_val = res [i].value;
-                               // we might not have a `.value`, like in case of getters which have a `.get` instead
-                               if (res_val !== undefined && res_val.isValueType != undefined && res_val.isValueType)
-                                       res_val.objectId = `dotnet:valuetype:${objId}:${res [i].fieldOffset}`;
-                       }
-
-                       this.var_info = [];
+               /**
+                * @param  {number} idNum
+                * @param  {boolean} expandValueTypes
+                * @returns {object}
+                */
+               _get_object_properties: function(idNum, expandValueTypes) {
+                       let { res_ok, res } = this.mono_wasm_get_object_properties_info (idNum, expandValueTypes);
+                       if (!res_ok)
+                               throw new Error (`Failed to get properties for ${idNum}`);
+
+                       res = MONO._filter_automatic_properties (res);
+                       res = this._assign_vt_ids (res, v => ({ containerId: idNum, fieldOffset: v.fieldOffset }));
+                       res = this._post_process_details (res);
 
                        return res;
                },
 
-               mono_wasm_get_array_values: function(objId) {
-                       if (!this.mono_wasm_get_array_values_info)
-                               this.mono_wasm_get_array_values_info = Module.cwrap ("mono_wasm_get_array_values", null, [ 'number' ]);
-
-                       this.var_info = [];
-                       this.mono_wasm_get_array_values_info (objId);
-
-                       var res = MONO._fixup_name_value_objects (this.var_info);
-                       for (var i = 0; i < res.length; i++) {
-                               var prop_value = res [i].value;
-                               if (prop_value.isValueType) {
-                                       res [i].value.objectId = `dotnet:array:${objId}:${i}`;
-                               } else if (prop_value.objectId !== undefined && prop_value.objectId.startsWith("dotnet:pointer")) {
-                                       prop_value.objectId = this._get_updated_ptr_id (prop_value.objectId, {
-                                               varName: `[${i}]`
-                                       });
-                               }
+               /**
+                * @param  {WasmId} id
+                * @param  {number} [startIdx=0]
+                * @param  {number} [count=-1]
+                * @param  {boolean} [expandValueTypes=false]
+                * @returns {object[]}
+                */
+               _get_array_values: function (id, startIdx = 0, count = -1, expandValueTypes = false) {
+                       if (isNaN (id.o.arrayId) || isNaN (startIdx))
+                               throw new Error (`Invalid array id: ${id.idStr}`);
+
+                       let { res_ok, res } = this.mono_wasm_get_array_values_info (id.o.arrayId, startIdx, count, expandValueTypes);
+                       if (!res_ok)
+                               throw new Error (`Failed to get properties for array id ${id.idStr}`);
+
+                       res = this._assign_vt_ids (res, (_, i) => ({ arrayId: id.o.arrayId, arrayIdx: Number (startIdx) + i}));
+
+                       for (let i = 0; i < res.length; i ++) {
+                               let value = res [i].value;
+                               if (value.objectId !== undefined && value.objectId.startsWith("dotnet:pointer"))
+                                       this._new_or_add_id_props ({ objectId: value.objectId, props: { varName: `[${i}]` } });
                        }
-
-                       this.var_info = [];
-
-                       return res;
-               },
-
-               mono_wasm_get_array_value_expanded: function(objId, idx) {
-                       if (!this.mono_wasm_get_array_value_expanded_info)
-                               this.mono_wasm_get_array_value_expanded_info = Module.cwrap ("mono_wasm_get_array_value_expanded", null, [ 'number', 'number' ]);
-
-                       this.var_info = [];
-                       this.mono_wasm_get_array_value_expanded_info (objId, idx);
-
-                       var res = MONO._fixup_name_value_objects (this.var_info);
-                       // length should be exactly one!
-                       if (res [0].value.isValueType != undefined && res [0].value.isValueType)
-                               res [0].value.objectId = `dotnet:array:${objId}:${idx}`;
-
-                       this.var_info = [];
-
+                       res = this._post_process_details (res);
                        return res;
                },
 
@@ -289,8 +337,13 @@ var MonoSupportLib = {
                        return details;
                },
 
-               _next_value_type_id: function () {
-                       return ++this._next_value_type_id_var;
+               /**
+                * Gets the next id number to use for generating ids
+                *
+                * @returns {number}
+                */
+               _next_id: function () {
+                       return ++this._next_id_var;
                },
 
                _extract_and_cache_value_types: function (var_list) {
@@ -298,82 +351,46 @@ var MonoSupportLib = {
                                return var_list;
 
                        for (let i in var_list) {
-                               var value = var_list [i].value;
+                               let value = var_list [i].value;
                                if (value === undefined)
                                        continue;
 
                                if (value.objectId !== undefined && value.objectId.startsWith ("dotnet:pointer:")) {
-                                       var ptr_args = this._get_ptr_args (value.objectId);
-                                       if (ptr_args.varName === undefined) {
-                                               // It might have been already set in some cases, like arrays
-                                               // where the name would be `0`, but we want `[0]` for pointers,
-                                               // so the deref would look like `*[0]`
-                                               value.objectId = this._get_updated_ptr_id (value.objectId, {
-                                                       varName: var_list [i].name
-                                               });
-                                       }
+                                       let ptr_args = this._get_id_props (value.objectId);
+                                       if (ptr_args === undefined)
+                                               throw new Error (`Bug: Expected to find an entry for pointer id: ${value.objectId}`);
+
+                                       // It might have been already set in some cases, like arrays
+                                       // where the name would be `0`, but we want `[0]` for pointers,
+                                       // so the deref would look like `*[0]`
+                                       ptr_args.varName = ptr_args.varName || var_list [i].name;
                                }
 
                                if (value.type != "object" || value.isValueType != true || value.expanded != true) // undefined would also give us false
                                        continue;
 
                                // Generate objectId for expanded valuetypes
-
-                               var objectId = value.objectId;
-                               if (objectId == undefined)
-                                       objectId = `dotnet:valuetype:${this._next_value_type_id ()}`;
-                               value.objectId = objectId;
+                               value.objectId = value.objectId || this._new_or_add_id_props ({ scheme: 'valuetype' });
 
                                this._extract_and_cache_value_types (value.members);
 
-                               this._value_types_cache [objectId] = value.members;
+                               const new_props = Object.assign ({ members: value.members }, value.__extra_vt_props);
+
+                               this._new_or_add_id_props ({ objectId: value.objectId, props: new_props });
                                delete value.members;
+                               delete value.__extra_vt_props;
                        }
 
                        return var_list;
                },
 
-               _get_details_for_value_type: function (objectId, fetchDetailsFn) {
-                       if (objectId in this._value_types_cache)
-                               return this._value_types_cache[objectId];
-
-                       this._post_process_details (fetchDetailsFn());
-                       if (objectId in this._value_types_cache)
-                               return this._value_types_cache[objectId];
-
-                       // return error
-                       throw new Error (`Could not get details for ${objectId}`);
-               },
-
-               _is_object_id_array: function (objectId) {
-                       // Keep this in sync with `_get_array_details`
-                       return (objectId.startsWith ('dotnet:array:') && objectId.split (':').length == 3);
-               },
-
-               _get_array_details: function (objectId, objectIdParts) {
-                       // Keep this in sync with `_is_object_id_array`
-                       switch (objectIdParts.length) {
-                               case 3:
-                                       return this._post_process_details (this.mono_wasm_get_array_values(objectIdParts[2]));
-
-                               case 4:
-                                       var arrayObjectId = objectIdParts[2];
-                                       var arrayIdx = objectIdParts[3];
-                                       return this._get_details_for_value_type(
-                                                                       objectId, () => this.mono_wasm_get_array_value_expanded(arrayObjectId, arrayIdx));
-
-                               default:
-                                       throw new Error (`object id format not supported : ${objectId}`);
-                       }
-               },
-
                _get_cfo_res_details: function (objectId, args) {
                        if (!(objectId in this._call_function_res_cache))
                                throw new Error(`Could not find any object with id ${objectId}`);
 
-                       var real_obj = this._call_function_res_cache [objectId];
+                       const real_obj = this._call_function_res_cache [objectId];
 
-                       var descriptors = Object.getOwnPropertyDescriptors (real_obj);
+                       const descriptors = Object.getOwnPropertyDescriptors (real_obj);
                        if (args.accessorPropertiesOnly) {
                                Object.keys (descriptors).forEach (k => {
                                        if (descriptors [k].get === undefined)
@@ -381,10 +398,10 @@ var MonoSupportLib = {
                                });
                        }
 
-                       var res_details = [];
+                       let res_details = [];
                        Object.keys (descriptors).forEach (k => {
-                               var new_obj;
-                               var prop_desc = descriptors [k];
+                               let new_obj;
+                               let prop_desc = descriptors [k];
                                if (typeof prop_desc.value == "object") {
                                        // convert `{value: { type='object', ... }}`
                                        // to      `{ name: 'foo', value: { type='object', ... }}
@@ -425,34 +442,77 @@ var MonoSupportLib = {
                        return { __value_as_json_string__: JSON.stringify (res_details) };
                },
 
-               _get_ptr_args: function (objectId) {
-                       var parts = this._split_object_id (objectId);
-                       if (parts.length != 3)
-                               throw new Error (`Bug: Unexpected objectId format for a pointer, expected 3 parts: ${objectId}`);
-                       return JSON.parse (parts [2]);
-               },
+               /**
+                * Generates a new id, and a corresponding entry for associated properties
+                *    like `dotnet:pointer:{ a: 4 }`
+                * The third segment of that `{a:4}` is the idArgs parameter
+                *
+                * Only `scheme` or `objectId` can be set.
+                * if `scheme`, then a new id is generated, and it's properties set
+                * if `objectId`, then it's properties are updated
+                *
+                * @param {object} args
+                * @param  {string} [args.scheme=undefined] scheme second part of `dotnet:pointer:..`
+                * @param  {string} [args.objectId=undefined] objectId
+                * @param  {object} [args.idArgs={}] The third segment of the objectId
+                * @param  {object} [args.props={}] Properties for the generated id
+                *
+                * @returns {string} generated/updated id string
+                */
+               _new_or_add_id_props: function ({ scheme = undefined, objectId = undefined, idArgs = {}, props = {} }) {
+                       if (scheme === undefined && objectId === undefined)
+                               throw new Error (`Either scheme or objectId must be given`);
+
+                       if (scheme !== undefined && objectId !== undefined)
+                               throw new Error (`Both scheme, and objectId cannot be given`);
+
+                       if (objectId !== undefined && Object.entries (idArgs).length > 0)
+                               throw new Error (`Both objectId, and idArgs cannot be given`);
+
+                       if (Object.entries (idArgs).length == 0) {
+                               // We want to generate a new id, only if it doesn't have other
+                               // attributes that it can use to uniquely identify.
+                               // Eg, we don't do this for `dotnet:valuetype:{containerId:4, fieldOffset: 24}`
+                               idArgs.num = this._next_id ();
+                       }
 
-               _get_updated_ptr_id: function (objectId, new_args) {
-                       var old_args = {};
-                       if (typeof (objectId) === 'string' && objectId.length)
-                               old_args = this._get_ptr_args (objectId);
+                       let idStr;
+                       if (objectId !== undefined) {
+                               idStr = objectId;
+                               const old_props = this._id_table [idStr];
+                               if (old_props === undefined)
+                                       throw new Error (`ObjectId not found in the id table: ${idStr}`);
 
-                       return `dotnet:pointer:${JSON.stringify ( Object.assign (old_args, new_args) )}`;
+                               this._id_table [idStr] = Object.assign (old_props, props);
+                       } else {
+                               idStr = `dotnet:${scheme}:${JSON.stringify (idArgs)}`;
+                               this._id_table [idStr] = props;
+                       }
+
+                       return idStr;
+               },
+
+               /**
+                * @param  {string} objectId
+                * @returns {object}
+                */
+               _get_id_props: function (objectId) {
+                       return this._id_table [objectId];
                },
 
                _get_deref_ptr_value: function (objectId) {
-                       if (!this.mono_wasm_get_deref_ptr_value_info)
-                               this.mono_wasm_get_deref_ptr_value_info = Module.cwrap("mono_wasm_get_deref_ptr_value", null, ['number', 'number']);
+                       const ptr_args = this._get_id_props (objectId);
+                       if (ptr_args === undefined)
+                               throw new Error (`Unknown pointer id: ${objectId}`);
 
-                       var ptr_args = this._get_ptr_args (objectId);
                        if (ptr_args.ptr_addr == 0 || ptr_args.klass_addr == 0)
                                throw new Error (`Both ptr_addr and klass_addr need to be non-zero, to dereference a pointer. objectId: ${objectId}`);
 
-                       this.var_info = [];
-                       var value_addr = new DataView (Module.HEAPU8.buffer).getUint32 (ptr_args.ptr_addr, /* littleEndian */ true);
-                       this.mono_wasm_get_deref_ptr_value_info (value_addr, ptr_args.klass_addr);
+                       const value_addr = new DataView (Module.HEAPU8.buffer).getUint32 (ptr_args.ptr_addr, /* littleEndian */ true);
+                       let { res_ok, res } = this.mono_wasm_get_deref_ptr_value_info (value_addr, ptr_args.klass_addr);
+                       if (!res_ok)
+                               throw new Error (`Failed to dereference pointer ${objectId}`);
 
-                       var res = MONO._fixup_name_value_objects(this.var_info);
                        if (res.length > 0) {
                                if (ptr_args.varName === undefined)
                                        throw new Error (`Bug: no varName found for the pointer. objectId: ${objectId}`);
@@ -461,34 +521,25 @@ var MonoSupportLib = {
                        }
 
                        res = this._post_process_details (res);
-                       this.var_info = [];
                        return res;
                },
 
                mono_wasm_get_details: function (objectId, args) {
-                       var parts = objectId.split(":");
-                       if (parts[0] != "dotnet")
-                               throw new Error ("Can't handle non-dotnet object ids. ObjectId: " + objectId);
+                       let id = this._parse_object_id (objectId, true);
 
-                       switch (parts[1]) {
-                               case "object":
-                                       if (parts.length != 3)
-                                               throw new Error(`exception this time: Invalid object id format: ${objectId}`);
+                       switch (id.scheme) {
+                               case "object": {
+                                       if (isNaN (id.value))
+                                               throw new Error (`Invalid objectId: ${objectId}. Expected a numeric id.`);
 
-                                       return this._post_process_details(this.mono_wasm_get_object_properties(parts[2], false));
+                                       return this._get_object_properties(id.value, false);
+                               }
 
                                case "array":
-                                       return this._get_array_details(objectId, parts);
+                                       return this._get_array_values (id);
 
                                case "valuetype":
-                                       if (parts.length != 3 && parts.length != 4) {
-                                               // dotnet:valuetype:vtid
-                                               // dotnet:valuetype:containerObjectId:vtId
-                                               throw new Error(`Invalid object id format: ${objectId}`);
-                                       }
-
-                                       var containerObjectId = parts[2];
-                                       return this._get_details_for_value_type(objectId, () => this.mono_wasm_get_object_properties(containerObjectId, true));
+                                       return this._get_vt_properties(id);
 
                                case "cfo_res":
                                        return this._get_cfo_res_details (objectId, args);
@@ -503,7 +554,7 @@ var MonoSupportLib = {
                },
 
                _cache_call_function_res: function (obj) {
-                       var id = `dotnet:cfo_res:${this._next_call_function_res_id++}`;
+                       const id = `dotnet:cfo_res:${this._next_call_function_res_id++}`;
                        this._call_function_res_cache[id] = obj;
                        return id;
                },
@@ -513,44 +564,69 @@ var MonoSupportLib = {
                                delete this._cache_call_function_res[objectId];
                },
 
-               _invoke_getter_on_object: function (objectId, name) {
-                       if (!this.mono_wasm_invoke_getter_on_object)
-                               this.mono_wasm_invoke_getter_on_object = Module.cwrap ("mono_wasm_invoke_getter_on_object", 'void', [ 'number', 'string' ]);
-
-                       if (objectId < 0) {
-                               // invalid id
-                               return [];
+               /**
+                * @param  {string} objectIdStr objectId
+                * @param  {string} name property name
+                * @returns {object} return value
+                */
+               _invoke_getter: function (objectIdStr, name) {
+                       const id = this._parse_object_id (objectIdStr);
+                       if (id === undefined)
+                               throw new Error (`Invalid object id: ${objectIdStr}`);
+
+                       let getter_res;
+                       if (id.scheme == 'object') {
+                               if (isNaN (id.o) || id.o < 0)
+                                       throw new Error (`Invalid object id: ${objectIdStr}`);
+
+                               let { res_ok, res } = this.mono_wasm_invoke_getter_on_object_info (id.o, name);
+                               if (!res_ok)
+                                       throw new Error (`Invoking getter on ${objectIdStr} failed`);
+
+                               getter_res = res;
+                       } else if (id.scheme == 'valuetype') {
+                               const id_props = this._get_id_props (objectIdStr);
+                               if (id_props === undefined)
+                                       throw new Error (`Unknown valuetype id: ${objectIdStr}`);
+
+                               if (typeof id_props.value64 !== 'string' || isNaN (id_props.klass))
+                                       throw new Error (`Bug: Cannot invoke getter on ${objectIdStr}, because of missing or invalid klass/value64 fields. idProps: ${JSON.stringify (id_props)}`);
+
+                               const dataPtr = Module._malloc (id_props.value64.length);
+                               const dataHeap = new Uint8Array (Module.HEAPU8.buffer, dataPtr, id_props.value64.length);
+                               dataHeap.set (new Uint8Array (this._base64_to_uint8 (id_props.value64)));
+
+                               let { res_ok, res } = this.mono_wasm_invoke_getter_on_value_info (dataHeap.byteOffset, id_props.klass, name);
+                               Module._free (dataHeap.byteOffset);
+
+                               if (!res_ok) {
+                                       console.debug (`Invoking getter on valuetype ${objectIdStr}, with props: ${JSON.stringify (id_props)} failed`);
+                                       throw new Error (`Invoking getter on valuetype ${objectIdStr} failed`);
+                               }
+                               getter_res = res;
+                       } else {
+                               throw new Error (`Only object, and valuetypes supported for getters, id: ${objectIdStr}`);
                        }
 
-                       this.mono_wasm_invoke_getter_on_object (objectId, name);
-                       var getter_res = MONO._post_process_details (MONO.var_info);
-
-                       MONO.var_info = [];
-                       return getter_res [0];
+                       getter_res = MONO._post_process_details (getter_res);
+                       return getter_res.length > 0 ? getter_res [0] : {};
                },
 
                _create_proxy_from_object_id: function (objectId) {
-                       var details = this.mono_wasm_get_details(objectId);
+                       const details = this.mono_wasm_get_details(objectId);
 
-                       if (this._is_object_id_array (objectId))
+                       if (objectId.startsWith ('dotnet:array:'))
                                return details.map (p => p.value);
 
-                       var objIdParts = objectId.split (':');
-                       var objIdNum = -1;
-                       if (objectId.startsWith ("dotnet:object:"))
-                               objIdNum = objIdParts [2];
-
-                       var proxy = {};
+                       let proxy = {};
                        Object.keys (details).forEach (p => {
                                var prop = details [p];
                                if (prop.get !== undefined) {
                                        // TODO: `set`
 
-                                       // We don't add a `get` for non-object types right now,
-                                       // so, we shouldn't get here with objIdNum==-1
                                        Object.defineProperty (proxy,
                                                        prop.name,
-                                                       { get () { return MONO._invoke_getter_on_object (objIdNum, prop.name); } }
+                                                       { get () { return MONO._invoke_getter (objectId, prop.name); } }
                                        );
                                } else {
                                        proxy [prop.name] = prop.value;
@@ -564,21 +640,27 @@ var MonoSupportLib = {
                        if (request.arguments != undefined && !Array.isArray (request.arguments))
                                throw new Error (`"arguments" should be an array, but was ${request.arguments}`);
 
-                       var objId = request.objectId;
-                       var proxy;
+                       const objId = request.objectId;
+                       let proxy;
 
-                       if (objId in this._call_function_res_cache) {
-                               proxy = this._call_function_res_cache [objId];
-                       } else if (!objId.startsWith ('dotnet:cfo_res:')) {
+                       if (objId.startsWith ('dotnet:cfo_res:')) {
+                               if (objId in this._call_function_res_cache)
+                                       proxy = this._call_function_res_cache [objId];
+                               else
+                                       throw new Error (`Unknown object id ${objId}`);
+                       } else {
                                proxy = this._create_proxy_from_object_id (objId);
                        }
 
-                       var fn_args = request.arguments != undefined ? request.arguments.map(a => JSON.stringify(a.value)) : [];
-                       var fn_eval_str = `var fn = ${request.functionDeclaration}; fn.call (proxy, ...[${fn_args}]);`;
+                       const fn_args = request.arguments != undefined ? request.arguments.map(a => JSON.stringify(a.value)) : [];
+                       const fn_eval_str = `var fn = ${request.functionDeclaration}; fn.call (proxy, ...[${fn_args}]);`;
 
-                       var fn_res = eval (fn_eval_str);
-                       if (fn_res == undefined) // should we just return undefined?
-                               throw Error ('Function returned undefined result');
+                       const fn_res = eval (fn_eval_str);
+                       if (fn_res === undefined)
+                               return { type: "undefined" };
+
+                       if (fn_res === null || (fn_res.subtype === 'null' && fn_res.value === undefined))
+                               return fn_res;
 
                        // primitive type
                        if (Object (fn_res) !== fn_res)
@@ -591,7 +673,7 @@ var MonoSupportLib = {
                        if (request.returnByValue)
                                return {type: "object", value: fn_res};
 
-                       var fn_res_id = this._cache_call_function_res (fn_res);
+                       const fn_res_id = this._cache_call_function_res (fn_res);
                        if (Object.getPrototypeOf (fn_res) == Array.prototype) {
                                return {
                                        type: "object",
@@ -606,14 +688,14 @@ var MonoSupportLib = {
                },
 
                _clear_per_step_state: function () {
-                       this._next_value_type_id_var = 0;
-                       this._value_types_cache = {};
+                       this._next_id_var = 0;
+                       this._id_table = {};
                },
 
                mono_wasm_debugger_resume: function () {
                        this._clear_per_step_state ();
                },
-               
+
                mono_wasm_start_single_stepping: function (kind) {
                        console.log (">> mono_wasm_start_single_stepping " + kind);
                        if (!this.mono_wasm_setup_single_step)
@@ -639,6 +721,42 @@ var MonoSupportLib = {
                        return this.mono_wasm_pause_on_exceptions (state_enum);
                },
 
+               _register_c_fn: function (name, ...args) {
+                       Object.defineProperty (this._c_fn_table, name + '_wrapper', { value: Module.cwrap (name, ...args) });
+               },
+
+               /**
+                * Calls `Module.cwrap` for the function name,
+                * and creates a wrapper around it that returns
+                *     `{ bool result, object var_info }
+                *
+                * @param  {string} name C function name
+                * @param  {string} ret_type
+                * @param  {string[]} params
+                *
+                * @returns {void}
+                */
+               _register_c_var_fn: function (name, ret_type, params) {
+                       if (ret_type !== 'bool')
+                               throw new Error (`Bug: Expected a C function signature that returns bool`);
+
+                       this._register_c_fn (name, ret_type, params);
+                       Object.defineProperty (this, name + '_info', {
+                               value: function (...args) {
+                                       MONO.var_info = [];
+                                       const res_ok = MONO._c_fn_table [name + '_wrapper'] (...args);
+                                       let res = MONO.var_info;
+                                       MONO.var_info = [];
+                                       if (res_ok) {
+                                               res = this._fixup_name_value_objects (res);
+                                               return { res_ok, res };
+                                       }
+
+                                       return { res_ok, res: undefined };
+                               }
+                       });
+               },
+
                mono_wasm_runtime_ready: function () {
                        this.mono_wasm_runtime_is_ready = true;
                        // DO NOT REMOVE - magic debugger init function
@@ -649,6 +767,14 @@ var MonoSupportLib = {
                        // FIXME: where should this go?
                        this._next_call_function_res_id = 0;
                        this._call_function_res_cache = {};
+
+                       this._c_fn_table = {};
+                       this._register_c_var_fn ('mono_wasm_get_object_properties',   'bool', [ 'number', 'bool' ]);
+                       this._register_c_var_fn ('mono_wasm_get_array_values',        'bool', [ 'number', 'number', 'number', 'bool' ]);
+                       this._register_c_var_fn ('mono_wasm_invoke_getter_on_object', 'bool', [ 'number', 'string' ]);
+                       this._register_c_var_fn ('mono_wasm_invoke_getter_on_value',  'bool', [ 'number', 'number', 'string' ]);
+                       this._register_c_var_fn ('mono_wasm_get_local_vars',          'bool', [ 'number', 'number', 'number']);
+                       this._register_c_var_fn ('mono_wasm_get_deref_ptr_value',     'bool', [ 'number', 'number']);
                },
 
                mono_wasm_set_breakpoint: function (assembly, method_token, il_offset) {
@@ -666,7 +792,7 @@ var MonoSupportLib = {
                },
 
                // Set environment variable NAME to VALUE
-               // Should be called before mono_load_runtime_and_bcl () in most cases 
+               // Should be called before mono_load_runtime_and_bcl () in most cases
                mono_wasm_setenv: function (name, value) {
                        if (!this.wasm_setenv)
                                this.wasm_setenv = Module.cwrap ('mono_wasm_setenv', null, ['string', 'string']);
@@ -678,7 +804,7 @@ var MonoSupportLib = {
                                this.wasm_parse_runtime_options = Module.cwrap ('mono_wasm_parse_runtime_options', null, ['number', 'number']);
                        var argv = Module._malloc (options.length * 4);
                        var wasm_strdup = Module.cwrap ('mono_wasm_strdup', 'number', ['string']);
-                       aindex = 0;
+                       let aindex = 0;
                        for (var i = 0; i < options.length; ++i) {
                                Module.setValue (argv + (aindex * 4), wasm_strdup (options [i]), "i32");
                                aindex += 1;
@@ -1155,7 +1281,7 @@ var MonoSupportLib = {
 
                mono_wasm_add_null_var: function(className)
                {
-                       fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className));
+                       let fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className));
                        if (!fixed_class_name) {
                                // Eg, when a @className is passed from js itself, like
                                // mono_wasm_add_null_var ("string")
@@ -1179,12 +1305,13 @@ var MonoSupportLib = {
                                value: {
                                        type: "string",
                                        value: var_value,
+                                       description: var_value
                                }
                        });
                },
 
                _mono_wasm_add_getter_var: function(className, invokable) {
-                       fixed_class_name = MONO._mono_csharp_fixup_class_name (className);
+                       const fixed_class_name = MONO._mono_csharp_fixup_class_name (className);
                        if (invokable != 0) {
                                var name;
                                if (MONO.var_info.length > 0)
@@ -1211,7 +1338,7 @@ var MonoSupportLib = {
                },
 
                _mono_wasm_add_array_var: function(className, objectId, length) {
-                       fixed_class_name = MONO._mono_csharp_fixup_class_name(className);
+                       const fixed_class_name = MONO._mono_csharp_fixup_class_name(className);
                        if (objectId == 0) {
                                MONO.mono_wasm_add_null_var (fixed_class_name);
                                return;
@@ -1223,42 +1350,121 @@ var MonoSupportLib = {
                                        subtype: "array",
                                        className: fixed_class_name,
                                        description: `${fixed_class_name}(${length})`,
-                                       objectId: "dotnet:array:"+ objectId,
+                                       objectId: this._new_or_add_id_props ({ scheme: 'array', idArgs: { arrayId: objectId } })
                                }
                        });
                },
 
+               // FIXME: improve
+               _base64_to_uint8: function (base64String) {
+                       const byteCharacters = atob (base64String);
+                       const byteNumbers = new Array(byteCharacters.length);
+                       for (let i = 0; i < byteCharacters.length; i++) {
+                               byteNumbers[i] = byteCharacters.charCodeAt(i);
+                       }
+
+                       return new Uint8Array (byteNumbers);
+               },
+
+               _begin_value_type_var: function(className, args) {
+                       if (args === undefined || (typeof args !== 'object')) {
+                               console.debug (`_begin_value_type_var: Expected an args object`);
+                               return;
+                       }
+
+                       const fixed_class_name = MONO._mono_csharp_fixup_class_name(className);
+                       const toString = args.toString;
+                       const base64String = btoa (String.fromCharCode (...new Uint8Array (Module.HEAPU8.buffer, args.value_addr, args.value_size)));
+                       const vt_obj = {
+                               value: {
+                                       type            : "object",
+                                       className       : fixed_class_name,
+                                       description     : (toString == 0 ? fixed_class_name: Module.UTF8ToString (toString)),
+                                       expanded        : true,
+                                       isValueType     : true,
+                                       __extra_vt_props: { klass: args.klass, value64: base64String },
+                                       members         : []
+                               }
+                       };
+                       if (MONO._vt_stack.length == 0)
+                               MONO._old_var_info = MONO.var_info;
+
+                       MONO.var_info = vt_obj.value.members;
+                       MONO._vt_stack.push (vt_obj);
+               },
+
+               _end_value_type_var: function() {
+                       let top_vt_obj_popped = MONO._vt_stack.pop ();
+                       top_vt_obj_popped.value.members = MONO._filter_automatic_properties (
+                                                               MONO._fixup_name_value_objects (top_vt_obj_popped.value.members));
+
+                       if (MONO._vt_stack.length == 0) {
+                               MONO.var_info = MONO._old_var_info;
+                               MONO.var_info.push(top_vt_obj_popped);
+                       } else {
+                               var top_obj = MONO._vt_stack [MONO._vt_stack.length - 1];
+                               top_obj.value.members.push (top_vt_obj_popped);
+                               MONO.var_info = top_obj.value.members;
+                       }
+               },
+
+               _add_valuetype_unexpanded_var: function(className, args) {
+                       if (args === undefined || (typeof args !== 'object')) {
+                               console.debug (`_add_valuetype_unexpanded_var: Expected an args object`);
+                               return;
+                       }
+
+                       const fixed_class_name = MONO._mono_csharp_fixup_class_name (className);
+                       const toString = args.toString;
+
+                       MONO.var_info.push ({
+                               value: {
+                                       type: "object",
+                                       className: fixed_class_name,
+                                       description: (toString == 0 ? fixed_class_name : Module.UTF8ToString (toString)),
+                                       isValueType: true
+                               }
+                       });
+               },
+
+
                mono_wasm_add_typed_value: function (type, str_value, value) {
-                       var type_str = type;
+                       let type_str = type;
                        if (typeof type != 'string')
                                type_str = Module.UTF8ToString (type);
-                       if (typeof str_value != 'string')
                                str_value = Module.UTF8ToString (str_value);
 
                        switch (type_str) {
-                       case "bool":
+                       case "bool": {
+                               const v = value != 0;
                                MONO.var_info.push ({
                                        value: {
                                                type: "boolean",
-                                               value: value != 0
+                                               value: v,
+                                               description: v.toString ()
                                        }
                                });
                                break;
+                       }
 
-                       case "char":
+                       case "char": {
+                               const v = `${value} '${String.fromCharCode (value)}'`;
                                MONO.var_info.push ({
                                        value: {
                                                type: "symbol",
-                                               value: `${value} '${String.fromCharCode (value)}'`
+                                               value: v,
+                                               description: v
                                        }
                                });
                                break;
+                       }
 
                        case "number":
                                MONO.var_info.push ({
                                        value: {
                                                type: "number",
-                                               value: value
+                                               value: value,
+                                               description: '' + value
                                        }
                                });
                                break;
@@ -1275,8 +1481,20 @@ var MonoSupportLib = {
                                MONO._mono_wasm_add_array_var (str_value, value.objectId, value.length);
                                break;
 
+                       case "begin_vt":
+                               MONO._begin_value_type_var (str_value, value);
+                               break;
+
+                       case "end_vt":
+                               MONO._end_value_type_var ();
+                               break;
+
+                       case "unexpanded_vt":
+                               MONO._add_valuetype_unexpanded_var (str_value, value);
+                               break;
+
                        case "pointer": {
-                               var fixed_value_str = MONO._mono_csharp_fixup_class_name (str_value);
+                               const fixed_value_str = MONO._mono_csharp_fixup_class_name (str_value);
                                if (value.klass_addr == 0 || value.ptr_addr == 0 || fixed_value_str.startsWith ('(void*')) {
                                        // null or void*, which we can't deref
                                        MONO.var_info.push({
@@ -1292,7 +1510,7 @@ var MonoSupportLib = {
                                                        type: "object",
                                                        className: fixed_value_str,
                                                        description: fixed_value_str,
-                                                       objectId: this._get_updated_ptr_id ('', value)
+                                                       objectId: this._new_or_add_id_props ({ scheme: 'pointer', props: value })
                                                }
                                        });
                                }
@@ -1300,7 +1518,7 @@ var MonoSupportLib = {
                                break;
 
                        default: {
-                               var msg = `'${str_value}' ${value}`;
+                               const msg = `'${str_value}' ${value}`;
 
                                MONO.var_info.push ({
                                        value: {
@@ -1390,55 +1608,6 @@ var MonoSupportLib = {
                MONO._async_method_objectId = objectId;
        },
 
-       mono_wasm_begin_value_type_var: function(className, toString) {
-               fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className));
-               var vt_obj = {
-                       value: {
-                               type: "object",
-                               className: fixed_class_name,
-                               description: (toString == 0 ? fixed_class_name : Module.UTF8ToString (toString)),
-                               // objectId will be generated by MonoProxy
-                               expanded: true,
-                               isValueType: true,
-                               members: []
-                       }
-               };
-               if (MONO._vt_stack.length == 0)
-                       MONO._old_var_info = MONO.var_info;
-
-               MONO.var_info = vt_obj.value.members;
-               MONO._vt_stack.push (vt_obj);
-       },
-
-       mono_wasm_end_value_type_var: function() {
-               var top_vt_obj_popped = MONO._vt_stack.pop ();
-               top_vt_obj_popped.value.members = MONO._filter_automatic_properties (
-                                                       MONO._fixup_name_value_objects (top_vt_obj_popped.value.members));
-
-               if (MONO._vt_stack.length == 0) {
-                       MONO.var_info = MONO._old_var_info;
-                       MONO.var_info.push(top_vt_obj_popped);
-               } else {
-                       var top_obj = MONO._vt_stack [MONO._vt_stack.length - 1];
-                       top_obj.value.members.push (top_vt_obj_popped);
-                       MONO.var_info = top_obj.value.members;
-               }
-       },
-
-       mono_wasm_add_value_type_unexpanded_var: function (className, toString) {
-               fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className));
-               MONO.var_info.push({
-                       value: {
-                               type: "object",
-                               className: fixed_class_name,
-                               description: (toString == 0 ? fixed_class_name : Module.UTF8ToString (toString)),
-                               // objectId added when enumerating object's properties
-                               expanded: false,
-                               isValueType: true
-                       }
-               });
-       },
-
        mono_wasm_add_enum_var: function(className, members, value) {
                // FIXME: flags
                //
@@ -1446,13 +1615,13 @@ var MonoSupportLib = {
                // group0: Monday:0
                // group1: Monday
                // group2: 0
-               var re = new RegExp (`[,]?([^,:]+):(${value}(?=,)|${value}$)`, 'g')
-               var members_str = Module.UTF8ToString (members);
+               const re = new RegExp (`[,]?([^,:]+):(${value}(?=,)|${value}$)`, 'g')
+               const members_str = Module.UTF8ToString (members);
 
-               var match = re.exec(members_str);
-               var member_name = match == null ? ('' + value) : match [1];
+               const match = re.exec(members_str);
+               const member_name = match == null ? ('' + value) : match [1];
 
-               fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className));
+               const fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className));
                MONO.var_info.push({
                        value: {
                                type: "object",
@@ -1475,7 +1644,7 @@ var MonoSupportLib = {
                        return;
                }
 
-               fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className));
+               const fixed_class_name = MONO._mono_csharp_fixup_class_name(Module.UTF8ToString (className));
                MONO.var_info.push({
                        value: {
                                type: "object",
@@ -1512,11 +1681,11 @@ var MonoSupportLib = {
                        return `${ret_sig} ${method_name} (${args_sig})`;
                }
 
-               var tgt_sig;
+               let tgt_sig;
                if (targetName != 0)
                        tgt_sig = args_to_sig (Module.UTF8ToString (targetName));
 
-               var type_name = MONO._mono_csharp_fixup_class_name (Module.UTF8ToString (className));
+               const type_name = MONO._mono_csharp_fixup_class_name (Module.UTF8ToString (className));
 
                if (objectId == -1) {
                        // Target property
@@ -1586,6 +1755,7 @@ var MonoSupportLib = {
 
        mono_wasm_fire_bp: function () {
                console.log ("mono_wasm_fire_bp");
+               // eslint-disable-next-line no-debugger
                debugger;
        },