Use HandleAllocator for GL object. NFC
diff --git a/src/library_glemu.js b/src/library_glemu.js
index b5cf300..a06ae23 100644
--- a/src/library_glemu.js
+++ b/src/library_glemu.js
@@ -564,15 +564,15 @@
         GL.shaderSources[shader] = source;
         dbg("glShaderSource: Output: \n" + source);
 #endif
-        GLctx.shaderSource(GL.shaders[shader], source);
+        GLctx.shaderSource(GL.shaders.get(shader), source);
       };
 
       var glCompileShader = _glCompileShader;
       _glCompileShader = _emscripten_glCompileShader = (shader) => {
-        GLctx.compileShader(GL.shaders[shader]);
+        GLctx.compileShader(GL.shaders.get(shader));
 #if GL_DEBUG
-        if (!GLctx.getShaderParameter(GL.shaders[shader], GLctx.COMPILE_STATUS)) {
-          dbg(`Failed to compile shader: ${GLctx.getShaderInfoLog(GL.shaders[shader])}`);
+        if (!GLctx.getShaderParameter(GL.shaders.get(shader), GLctx.COMPILE_STATUS)) {
+          dbg(`Failed to compile shader: ${GLctx.getShaderInfoLog(GL.shaders.get(shader))}`);
           dbg(`Info: ${JSON.stringify(GL.shaderInfos[shader])}`);
           dbg(`Original source: ${GL.shaderOriginalSources[shader]}`);
           dbg(`Source: ${GL.shaderSources[shader]}`);
@@ -642,7 +642,7 @@
       var glLinkProgram = _glLinkProgram;
       _glLinkProgram = _emscripten_glLinkProgram = (program) => {
         if (!(program in zeroUsedPrograms)) {
-          GLctx.bindAttribLocation(GL.programs[program], 0, 'a_position');
+          GLctx.bindAttribLocation(GL.programs.get(program), 0, 'a_position');
         }
         glLinkProgram(program);
       };
@@ -747,9 +747,9 @@
 
   glDeleteObject__deps: ['glDeleteProgram', 'glDeleteShader'],
   glDeleteObject: (id) => {
-    if (GL.programs[id]) {
+    if (GL.programs.has(id)) {
       _glDeleteProgram(id);
-    } else if (GL.shaders[id]) {
+    } else if (GL.shaders.has(id)) {
       _glDeleteShader(id);
     } else {
       err(`WARNING: deleteObject received invalid id: ${id}`);
@@ -759,22 +759,22 @@
 
   glGetObjectParameteriv__deps: ['glGetProgramiv', 'glGetShaderiv'],
   glGetObjectParameteriv: (id, type, result) => {
-    if (GL.programs[id]) {
+    if (GL.programs.has(id)) {
       if (type == 0x8B84) { // GL_OBJECT_INFO_LOG_LENGTH_ARB
-        var log = GLctx.getProgramInfoLog(GL.programs[id]);
+        var log = GLctx.getProgramInfoLog(GL.programs.get(id));
         if (log === null) log = '(unknown error)';
         {{{ makeSetValue('result', '0', 'log.length', 'i32') }}};
         return;
       }
       _glGetProgramiv(id, type, result);
-    } else if (GL.shaders[id]) {
+    } else if (GL.shaders.has(id)) {
       if (type == 0x8B84) { // GL_OBJECT_INFO_LOG_LENGTH_ARB
-        var log = GLctx.getShaderInfoLog(GL.shaders[id]);
+        var log = GLctx.getShaderInfoLog(GL.shaders.get(id));
         if (log === null) log = '(unknown error)';
         {{{ makeSetValue('result', '0', 'log.length', 'i32') }}};
         return;
       } else if (type == 0x8B88) { // GL_OBJECT_SHADER_SOURCE_LENGTH_ARB
-        var source = GLctx.getShaderSource(GL.shaders[id]);
+        var source = GLctx.getShaderSource(GL.shaders.get(id));
         if (source === null) return; // If an error occurs, nothing will be written to result
         {{{ makeSetValue('result', '0', 'source.length', 'i32') }}};
         return;
@@ -788,9 +788,9 @@
 
   glGetInfoLog__deps: ['glGetProgramInfoLog', 'glGetShaderInfoLog'],
   glGetInfoLog: (id, maxLength, length, infoLog) => {
-    if (GL.programs[id]) {
+    if (GL.programs.has(id)) {
       _glGetProgramInfoLog(id, maxLength, length, infoLog);
-    } else if (GL.shaders[id]) {
+    } else if (GL.shaders.has(id)) {
       _glGetShaderInfoLog(id, maxLength, length, infoLog);
     } else {
       err(`WARNING: glGetInfoLog received invalid id: ${id}`);
@@ -2158,13 +2158,13 @@
 
           if (useCurrProgram) {
             if (GL.shaderInfos[GL.programShaders[GL.currProgram][0]].type == GLctx.VERTEX_SHADER) {
-              this.vertexShader = GL.shaders[GL.programShaders[GL.currProgram][0]];
-              this.fragmentShader = GL.shaders[GL.programShaders[GL.currProgram][1]];
+              this.vertexShader = GL.shaders.get(GL.programShaders[GL.currProgram][0]);
+              this.fragmentShader = GL.shaders.get(GL.programShaders[GL.currProgram][1]);
             } else {
-              this.vertexShader = GL.shaders[GL.programShaders[GL.currProgram][1]];
-              this.fragmentShader = GL.shaders[GL.programShaders[GL.currProgram][0]];
+              this.vertexShader = GL.shaders.get(GL.programShaders[GL.currProgram][1]);
+              this.fragmentShader = GL.shaders.get(GL.programShaders[GL.currProgram][0]);
             }
-            this.program = GL.programs[GL.currProgram];
+            this.program = GL.programs.get(GL.currProgram);
             this.usedTexUnitList = [];
           } else {
             // IMPORTANT NOTE: If you parameterize the shader source based on any runtime values
@@ -3533,30 +3533,25 @@
   $emulGlGenVertexArrays__deps: ['$GLEmulation'],
   $emulGlGenVertexArrays: (n, vaos) => {
     for (var i = 0; i < n; i++) {
-      var id = GL.getNewId(GLEmulation.vaos);
-      GLEmulation.vaos[id] = {
-        id,
+      var vao = {
         arrayBuffer: 0,
         elementArrayBuffer: 0,
         enabledVertexAttribArrays: {},
         vertexAttribPointers: {},
         enabledClientStates: {},
       };
-      {{{ makeSetValue('vaos', 'i*4', 'id', 'i32') }}};
+      vao.id = GLEmulation.vaos.allocate(vao);
+      {{{ makeSetValue('vaos', 'i*4', 'vao.id', 'i32') }}};
     }
   },
   $emulGlDeleteVertexArrays: (n, vaos) => {
     for (var i = 0; i < n; i++) {
       var id = {{{ makeGetValue('vaos', 'i*4', 'i32') }}};
-      GLEmulation.vaos[id] = null;
+      GLEmulation.vaos.free(id);
       if (GLEmulation.currentVao && GLEmulation.currentVao.id == id) GLEmulation.currentVao = null;
     }
   },
-  $emulGlIsVertexArray: (array) => {
-    var vao = GLEmulation.vaos[array];
-    if (!vao) return 0;
-    return 1;
-  },
+  $emulGlIsVertexArray: (array) => GLEmulation.vaos.has(array),
   $emulGlBindVertexArray__deps: ['glBindBuffer', 'glEnableVertexAttribArray', 'glVertexAttribPointer', 'glEnableClientState'],
   $emulGlBindVertexArray: (vao) => {
     // undo vao-related things, wipe the slate clean, both for vao of 0 or an actual vao
@@ -3573,7 +3568,7 @@
     GLImmediate.modifiedClientAttributes = true;
     if (vao) {
       // replay vao
-      var info = GLEmulation.vaos[vao];
+      var info = GLEmulation.vaos.get(vao);
       _glBindBuffer(GLctx.ARRAY_BUFFER, info.arrayBuffer); // XXX overwrite current binding?
       _glBindBuffer(GLctx.ELEMENT_ARRAY_BUFFER, info.elementArrayBuffer);
       for (var vaa in info.enabledVertexAttribArrays) {
diff --git a/src/library_html5_webgl.js b/src/library_html5_webgl.js
index 7e374f0..1124eca 100644
--- a/src/library_html5_webgl.js
+++ b/src/library_html5_webgl.js
@@ -425,26 +425,26 @@
 
   emscripten_webgl_get_program_parameter_d__proxy: 'sync_on_current_webgl_context_thread',
   emscripten_webgl_get_program_parameter_d: (program, param) =>
-    GLctx.getProgramParameter(GL.programs[program], param),
+    GLctx.getProgramParameter(GL.programs.get(program), param),
 
   emscripten_webgl_get_program_info_log_utf8__proxy: 'sync_on_current_webgl_context_thread',
   emscripten_webgl_get_program_info_log_utf8__deps: ['$stringToNewUTF8'],
   emscripten_webgl_get_program_info_log_utf8: (program) =>
-    stringToNewUTF8(GLctx.getProgramInfoLog(GL.programs[program])),
+    stringToNewUTF8(GLctx.getProgramInfoLog(GL.programs.get(program))),
 
   emscripten_webgl_get_shader_parameter_d__proxy: 'sync_on_current_webgl_context_thread',
   emscripten_webgl_get_shader_parameter_d: (shader, param) =>
-    GLctx.getShaderParameter(GL.shaders[shader], param),
+    GLctx.getShaderParameter(GL.shaders.get(shader), param),
 
   emscripten_webgl_get_shader_info_log_utf8__proxy: 'sync_on_current_webgl_context_thread',
   emscripten_webgl_get_shader_info_log_utf8__deps: ['$stringToNewUTF8'],
   emscripten_webgl_get_shader_info_log_utf8: (shader) =>
-    stringToNewUTF8(GLctx.getShaderInfoLog(GL.shaders[shader])),
+    stringToNewUTF8(GLctx.getShaderInfoLog(GL.shaders.get(shader))),
 
   emscripten_webgl_get_shader_source_utf8__proxy: 'sync_on_current_webgl_context_thread',
   emscripten_webgl_get_shader_source_utf8__deps: ['$stringToNewUTF8'],
   emscripten_webgl_get_shader_source_utf8: (shader) =>
-    stringToNewUTF8(GLctx.getShaderSource(GL.shaders[shader])),
+    stringToNewUTF8(GLctx.getShaderSource(GL.shaders.get(shader))),
 
   emscripten_webgl_get_vertex_attrib_d__proxy: 'sync_on_current_webgl_context_thread',
   emscripten_webgl_get_vertex_attrib_d: (index, param) =>
@@ -462,14 +462,13 @@
     writeGLArray(GLctx.getVertexAttrib(index, param), dst, dstLength, dstType),
 
   emscripten_webgl_get_uniform_d__proxy: 'sync_on_current_webgl_context_thread',
-  emscripten_webgl_get_uniform_d__deps: ['$webglGetUniformLocation'],
   emscripten_webgl_get_uniform_d: (program, location) =>
-    GLctx.getUniform(GL.programs[program], webglGetUniformLocation(location)),
+    GLctx.getUniform(GL.programs.get(program), webglGetUniformLocation(location)),
 
   emscripten_webgl_get_uniform_v__proxy: 'sync_on_current_webgl_context_thread',
   emscripten_webgl_get_uniform_v__deps: ['$writeGLArray', '$webglGetUniformLocation'],
   emscripten_webgl_get_uniform_v: (program, location, dst, dstLength, dstType) =>
-    writeGLArray(GLctx.getUniform(GL.programs[program], webglGetUniformLocation(location)), dst, dstLength, dstType),
+    writeGLArray(GLctx.getUniform(GL.programs.get(program), webglGetUniformLocation(location)), dst, dstLength, dstType),
 
   emscripten_webgl_get_parameter_v__proxy: 'sync_on_current_webgl_context_thread',
   emscripten_webgl_get_parameter_v__deps: ['$writeGLArray'],
diff --git a/src/library_webgl.js b/src/library_webgl.js
index 4fa7c4b..4944ede 100644
--- a/src/library_webgl.js
+++ b/src/library_webgl.js
@@ -102,7 +102,7 @@
   },
 
   emscripten_webgl_enable_ANGLE_instanced_arrays__deps: ['$webgl_enable_ANGLE_instanced_arrays'],
-  emscripten_webgl_enable_ANGLE_instanced_arrays: (ctx) => webgl_enable_ANGLE_instanced_arrays(GL.contexts[ctx].GLctx),
+  emscripten_webgl_enable_ANGLE_instanced_arrays: (ctx) => webgl_enable_ANGLE_instanced_arrays(GL.contexts.get(ctx).GLctx),
 
   $webgl_enable_OES_vertex_array_object: (ctx) => {
     // Extension available in WebGL 1 from Firefox 25 and WebKit 536.28/desktop Safari 6.0.3 onwards. Core feature in WebGL 2.
@@ -117,7 +117,7 @@
   },
 
   emscripten_webgl_enable_OES_vertex_array_object__deps: ['$webgl_enable_OES_vertex_array_object'],
-  emscripten_webgl_enable_OES_vertex_array_object: (ctx) => webgl_enable_OES_vertex_array_object(GL.contexts[ctx].GLctx),
+  emscripten_webgl_enable_OES_vertex_array_object: (ctx) => webgl_enable_OES_vertex_array_object(GL.contexts.get(ctx).GLctx),
 
   $webgl_enable_WEBGL_draw_buffers: (ctx) => {
     // Extension available in WebGL 1 from Firefox 28 onwards. Core feature in WebGL 2.
@@ -129,7 +129,7 @@
   },
 
   emscripten_webgl_enable_WEBGL_draw_buffers__deps: ['$webgl_enable_WEBGL_draw_buffers'],
-  emscripten_webgl_enable_WEBGL_draw_buffers: (ctx) => webgl_enable_WEBGL_draw_buffers(GL.contexts[ctx].GLctx),
+  emscripten_webgl_enable_WEBGL_draw_buffers: (ctx) => webgl_enable_WEBGL_draw_buffers(GL.contexts.get(ctx).GLctx),
 #endif
 
   $webgl_enable_WEBGL_multi_draw: (ctx) => {
@@ -138,7 +138,7 @@
   },
 
   emscripten_webgl_enable_WEBGL_multi_draw__deps: ['$webgl_enable_WEBGL_multi_draw'],
-  emscripten_webgl_enable_WEBGL_multi_draw: (ctx) => webgl_enable_WEBGL_multi_draw(GL.contexts[ctx].GLctx),
+  emscripten_webgl_enable_WEBGL_multi_draw: (ctx) => webgl_enable_WEBGL_multi_draw(GL.contexts.get(ctx).GLctx),
 
   $getEmscriptenSupportedExtensions__internal: true,
   $getEmscriptenSupportedExtensions: (ctx) => {
@@ -197,14 +197,19 @@
     return (ctx.getSupportedExtensions() || []).filter(ext => supportedExtensions.includes(ext));
   },
 
-  $GL__postset: 'var GLctx;',
-#if GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
-  // If GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS is enabled, GL.initExtensions() will call to initialize these.
+  $GL__postset: `
+    var GLctx;
+    GL.init();
+  `,
   $GL__deps: [
+    '$HandleAllocator',
 #if PTHREADS
     'malloc', // Needed by registerContext
     'free', // Needed by deleteContext
 #endif
+#if GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
+    // If GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS is enabled,
+    // GL.initExtensions() will call to initialize these.
 #if MIN_WEBGL_VERSION == 1
     '$webgl_enable_ANGLE_instanced_arrays',
     '$webgl_enable_OES_vertex_array_object',
@@ -216,8 +221,8 @@
 #endif
     '$webgl_enable_WEBGL_multi_draw',
     '$getEmscriptenSupportedExtensions',
-  ],
 #endif // GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
+  ],
   $GL: {
 #if GL_DEBUG
     debug: true,
@@ -236,23 +241,24 @@
     currElementArrayBuffer: 0,
 #endif
 */
+    init: () => {
+      GL.programs = new HandleAllocator();
+      GL.buffers = new HandleAllocator();
+      GL.framebuffers = new HandleAllocator();
+      GL.renderbuffers = new HandleAllocator();
+      GL.textures = new HandleAllocator();
+      GL.shaders = new HandleAllocator();
+#if PTHREADS // with pthreads a context is a location in memory with some synchronized data between threads
+      GL.contexts = new Map();
+#else            // without pthreads, it's just an integer ID
+      GL.contexts = new HandleAllocator();
+#endif
+    },
 
-    counter: 1, // 0 is reserved as 'null' in gl
-    buffers: [],
 #if FULL_ES3
     mappedBuffers: {},
 #endif
-    programs: [],
-    framebuffers: [],
-    renderbuffers: [],
-    textures: [],
-    shaders: [],
     vaos: [],
-#if PTHREADS // with pthreads a context is a location in memory with some synchronized data between threads
-    contexts: {},
-#else            // without pthreads, it's just an integer ID
-    contexts: [],
-#endif
     // DOM ID -> OffscreenCanvas mappings of <canvas> elements that have their
     // rendering control transferred to offscreen.
     offscreenCanvases: {},
@@ -299,15 +305,6 @@
       }
 #endif
     },
-    // Get a new ID for a texture/buffer/etc., while keeping the table dense and
-    // fast. Creation is fairly rare so it is worth optimizing lookups later.
-    getNewId: (table) => {
-      var ret = GL.counter++;
-      for (var i = table.length; i < ret; i++) {
-        table[i] = null;
-      }
-      return ret;
-    },
 
     // The code path for creating textures, buffers, framebuffers and other
     // objects the same (and not in fast path), so we merge the functions
@@ -317,22 +314,25 @@
     // created objects, and 'functionName' carries the name of the caller for
     // debug information.
     genObject: (n, buffers, createFunction, objectTable
-#if GL_ASSERTIONS
+  #if GL_ASSERTIONS
       , functionName
-#endif
+  #endif
       ) => {
       for (var i = 0; i < n; i++) {
         var buffer = GLctx[createFunction]();
-        var id = buffer && GL.getNewId(objectTable);
+        var id = 0;
         if (buffer) {
+          id = objectTable.allocate(buffer);
           buffer.name = id;
-          objectTable[id] = buffer;
         } else {
           GL.recordError(0x502 /* GL_INVALID_OPERATION */);
-#if GL_ASSERTIONS
+  #if GL_ASSERTIONS
           err(`GL_INVALID_OPERATION in ${functionName}: GLctx.${createFunction} returned null - most likely GL context is lost!`);
-#endif
+  #endif
         }
+#if GL_ASSERTIONS
+        dbg(`genObject (${functionName}) -> ${id}`);
+#endif
         {{{ makeSetValue('buffers', 'i*4', 'id', 'i32') }}};
       }
     },
@@ -470,7 +470,7 @@
       }
 #if LEGACY_GL_EMULATION
       // Let's see if we need to enable the standard derivatives extension
-      var type = GLctx.getShaderParameter(GL.shaders[shader], 0x8B4F /* GL_SHADER_TYPE */);
+      var type = GLctx.getShaderParameter(GL.shaders.get(shader), 0x8B4F /* GL_SHADER_TYPE */);
       if (type == 0x8B30 /* GL_FRAGMENT_SHADER */) {
         if (GLEmulation.findToken(source, "dFdx") ||
             GLEmulation.findToken(source, "dFdy") ||
@@ -541,19 +541,15 @@
 
     postDrawHandleClientVertexAttribBindings: () => {
       if (GL.resetBufferBinding) {
-        GLctx.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, GL.buffers[GLctx.currentArrayBufferBinding]);
+        GLctx.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, GL.buffers.get(GLctx.currentArrayBufferBinding));
       }
     },
 #endif
 
 #if GL_ASSERTIONS
-    validateGLObjectID: (objectHandleArray, objectID, callerFunctionName, objectReadableType) => {
-      if (objectID != 0) {
-        if (objectHandleArray[objectID] === null) {
-          err(`${callerFunctionName} called with an already deleted ${objectReadableType} ID ${objectID}!`);
-        } else if (!(objectID in objectHandleArray)) {
-          err(`${callerFunctionName} called with a nonexisting ${objectReadableType} ID ${objectID}!`);
-        }
+    validateGLHandle: (allocator, objectID, callerFunctionName, objectReadableType) => {
+      if (objectID != 0 && !allocator.has(objectID)) {
+        err(`${callerFunctionName} called with invalid ${objectReadableType} ID: ${objectID}!`);
       }
     },
     // Validates that user obeys GL spec #6.4: http://www.khronos.org/registry/webgl/specs/latest/1.0/#6.4
@@ -1079,34 +1075,32 @@
     },
 #endif
 
-    registerContext: (ctx, webGLContextAttributes) => {
-#if PTHREADS
-      // with pthreads a context is a location in memory with some synchronized
-      // data between threads
-      var handle = _malloc({{{ 2 * POINTER_SIZE }}});
-#if GL_ASSERTIONS
-      assert(handle, 'malloc() failed in GL.registerContext!');
-#endif
-#if GL_SUPPORT_EXPLICIT_SWAP_CONTROL
-      {{{ makeSetValue('handle', 0, 'webGLContextAttributes.explicitSwapControl', 'i32')}}};
-#endif
-      {{{ makeSetValue('handle', POINTER_SIZE, '_pthread_self()', '*')}}}; // the thread pointer of the thread that owns the control of the context
-#else // PTHREADS
-      // without pthreads a context is just an integer ID
-      var handle = GL.getNewId(GL.contexts);
-#endif // PTHREADS
-
+    registerContext: function(ctx, webGLContextAttributes) {
       var context = {
-        handle,
         attributes: webGLContextAttributes,
         version: webGLContextAttributes.majorVersion,
         GLctx: ctx
       };
+#if PTHREADS
+      // with pthreads a context is a location in memory with some synchronized
+      // data between threads
+      context.handle = _malloc({{{ 2 * POINTER_SIZE }}});
+      GL.contexts.set(context.handle, context);
+#if GL_ASSERTIONS
+      assert(handle, 'malloc() failed in GL.registerContext!');
+#endif
+#if GL_SUPPORT_EXPLICIT_SWAP_CONTROL
+      {{{ makeSetValue('context.handle', 0, 'webGLContextAttributes.explicitSwapControl', 'i32')}}}; // explicitSwapControl
+#endif
+      {{{ makeSetValue('context.handle', 4, '_pthread_self()', 'i32')}}}; // the thread pointer of the thread that owns the control of the context
+#else // PTHREADS
+      // without pthreads a context is just an integer ID
+      context.handle = GL.contexts.allocate(context);
+#endif // PTHREADS
 
       // Store the created context object so that we can access the context
       // given a canvas without having to pass the parameters again.
       if (ctx.canvas) ctx.canvas.GLctxObject = context;
-      GL.contexts[handle] = context;
 #if GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
       if (typeof webGLContextAttributes.enableExtensionsByDefault == 'undefined' || webGLContextAttributes.enableExtensionsByDefault) {
         GL.initExtensions(context);
@@ -1143,12 +1137,12 @@
 #endif
 
 #endif
-      return handle;
+      return context.handle;
     },
 
     makeContextCurrent: (contextHandle) => {
 #if GL_DEBUG
-      if (contextHandle && !GL.contexts[contextHandle]) {
+      if (contextHandle && !GL.contexts.has(contextHandle)) {
 #if PTHREADS
         dbg(`GL.makeContextCurrent() failed! WebGL context ${contextHandle} does not exist, or was created on another thread!`);
 #else
@@ -1157,35 +1151,24 @@
       }
 #endif
 
-      // Active Emscripten GL layer context object.
-      GL.currentContext = GL.contexts[contextHandle];
-      // Active WebGL context object.
-      Module.ctx = GLctx = GL.currentContext?.GLctx;
+      GL.currentContext = contextHandle ? GL.contexts.get(contextHandle) : contextHandle; // Active Emscripten GL layer context object.
+      Module.ctx = GLctx = GL.currentContext && GL.currentContext.GLctx; // Active WebGL context object.
       return !(contextHandle && !GLctx);
     },
 
-    getContext: (contextHandle) => {
-      return GL.contexts[contextHandle];
-    },
+    getContext: (contextHandle) => GL.contexts.get(contextHandle),
 
     deleteContext: (contextHandle) => {
-      if (GL.currentContext === GL.contexts[contextHandle]) {
-        GL.currentContext = null;
-      }
-      if (typeof JSEvents == 'object') {
-        // Release all JS event handlers on the DOM element that the GL context is
-        // associated with since the context is now deleted.
-        JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas);
-      }
-      // Make sure the canvas object no longer refers to the context object so
-      // there are no GC surprises.
-      if (GL.contexts[contextHandle] && GL.contexts[contextHandle].GLctx.canvas) {
-        GL.contexts[contextHandle].GLctx.canvas.GLctxObject = undefined;
-      }
+      var ctx = GL.contexts.get(contextHandle);
+      if (GL.currentContext === ctx) GL.currentContext = null;
+      if (typeof JSEvents == 'object') JSEvents.removeAllHandlersOnTarget(ctx.GLctx.canvas); // Release all JS event handlers on the DOM element that the GL context is associated with since the context is now deleted.
+      if (ctx && ctx.GLctx.canvas) ctx.GLctx.canvas.GLctxObject = undefined; // Make sure the canvas object no longer refers to the context object so there are no GC surprises.
 #if PTHREADS
-      _free(GL.contexts[contextHandle].handle);
+      _free(ctx.handle);
+      GL.contexts.delete(contextHandle);
+#else
+      GL.contexts.free(contextHandle);
 #endif
-      GL.contexts[contextHandle] = null;
     },
 
 #if GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
@@ -1537,13 +1520,13 @@
   glDeleteTextures: (n, textures) => {
     for (var i = 0; i < n; i++) {
       var id = {{{ makeGetValue('textures', 'i*4', 'i32') }}};
-      var texture = GL.textures[id];
       // GL spec: "glDeleteTextures silently ignores 0s and names that do not
       // correspond to existing textures".
-      if (!texture) continue;
+      if (!id || !GL.textures.has(id)) continue
+      var texture = GL.textures.get(id);
       GLctx.deleteTexture(texture);
       texture.name = 0;
-      GL.textures[id] = null;
+      GL.textures.free(id);
     }
   },
 
@@ -1749,9 +1732,9 @@
 
   glBindTexture: (target, texture) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.textures, texture, 'glBindTexture', 'texture');
+    GL.validateGLHandle(GL.textures, texture, 'glBindTexture', 'texture');
 #endif
-    GLctx.bindTexture(target, GL.textures[texture]);
+    GLctx.bindTexture(target, GL.textures.get(texture));
   },
 
   glGetTexParameterfv: (target, pname, params) => {
@@ -1793,8 +1776,8 @@
   },
 
   glIsTexture: (id) => {
-    var texture = GL.textures[id];
-    if (!texture) return 0;
+    if (!GL.textures.has(id)) return 0;
+    var texture = GL.textures.get(id);
     return GLctx.isTexture(texture);
   },
 
@@ -1817,15 +1800,15 @@
   glDeleteBuffers: (n, buffers) => {
     for (var i = 0; i < n; i++) {
       var id = {{{ makeGetValue('buffers', 'i*4', 'i32') }}};
-      var buffer = GL.buffers[id];
 
       // From spec: "glDeleteBuffers silently ignores 0's and names that do not
       // correspond to existing buffer objects."
-      if (!buffer) continue;
+      if (!GL.buffers.has(id)) continue;
 
+      var buffer = GL.buffers.get(id);
       GLctx.deleteBuffer(buffer);
       buffer.name = 0;
-      GL.buffers[id] = null;
+      GL.buffers.free(id);
 
 #if FULL_ES2 || LEGACY_GL_EMULATION
       if (id == GLctx.currentArrayBufferBinding) GLctx.currentArrayBufferBinding = 0;
@@ -1920,9 +1903,7 @@
         while (i < n) {{{ makeSetValue('ids', 'i++*4', 0, 'i32') }}};
         return;
       }
-      var id = GL.getNewId(GL.queries);
-      query.name = id;
-      GL.queries[id] = query;
+      query.name = GL.queries.allocate(query);
       {{{ makeSetValue('ids', 'i*4', 'id', 'i32') }}};
     }
   },
@@ -1948,7 +1929,7 @@
   glBeginQueryEXT__sig: 'vii',
   glBeginQueryEXT: (target, id) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.queries, id, 'glBeginQueryEXT', 'id');
+    GL.validateGLHandle(GL.queries, id, 'glBeginQueryEXT', 'id');
 #endif
     GLctx.disjointTimerQueryExt['beginQueryEXT'](target, GL.queries[id]);
   },
@@ -1964,7 +1945,7 @@
   glQueryCounterEXT__sig: 'vii',
   glQueryCounterEXT: (id, target) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.queries, id, 'glQueryCounterEXT', 'id');
+    GL.validateGLHandle(GL.queries, id, 'glQueryCounterEXT', 'id');
 #endif
     GLctx.disjointTimerQueryExt['queryCounterEXT'](GL.queries[id], target);
   },
@@ -1995,7 +1976,7 @@
       return;
     }
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.queries, id, 'glGetQueryObjectivEXT', 'id');
+    GL.validateGLHandle(GL.queries, id, 'glGetQueryObjectivEXT', 'id');
 #endif
     var query = GL.queries[id];
     var param = GLctx.disjointTimerQueryExt['getQueryObjectEXT'](query, pname);
@@ -2022,7 +2003,7 @@
       return;
     }
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.queries, id, 'glGetQueryObjecti64vEXT', 'id');
+    GL.validateGLHandle(GL.queries, id, 'glGetQueryObjecti64vEXT', 'id');
 #endif
     var query = GL.queries[id];
     var param;
@@ -2048,8 +2029,8 @@
   glGetQueryObjectui64vEXT: 'glGetQueryObjecti64vEXT',
 
   glIsBuffer: (buffer) => {
-    var b = GL.buffers[buffer];
-    if (!b) return 0;
+    if (!GL.buffers.has(buffer)) return 0;
+    var b = GL.buffers.get(buffer);
     return GLctx.isBuffer(b);
   },
 
@@ -2064,19 +2045,19 @@
   glDeleteRenderbuffers: (n, renderbuffers) => {
     for (var i = 0; i < n; i++) {
       var id = {{{ makeGetValue('renderbuffers', 'i*4', 'i32') }}};
-      var renderbuffer = GL.renderbuffers[id];
-      if (!renderbuffer) continue; // GL spec: "glDeleteRenderbuffers silently ignores 0s and names that do not correspond to existing renderbuffer objects".
+      if (!GL.renderbuffers.has(id)) continue; // GL spec: "glDeleteRenderbuffers silently ignores 0s and names that do not correspond to existing renderbuffer objects".
+      var renderbuffer = GL.renderbuffers.get(id);
       GLctx.deleteRenderbuffer(renderbuffer);
       renderbuffer.name = 0;
-      GL.renderbuffers[id] = null;
+      GL.renderbuffers.free(id);
     }
   },
 
   glBindRenderbuffer: (target, renderbuffer) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.renderbuffers, renderbuffer, 'glBindRenderbuffer', 'renderbuffer');
+    GL.validateGLHandle(GL.renderbuffers, renderbuffer, 'glBindRenderbuffer', 'renderbuffer');
 #endif
-    GLctx.bindRenderbuffer(target, GL.renderbuffers[renderbuffer]);
+    GLctx.bindRenderbuffer(target, GL.renderbuffers.get(renderbuffer));
   },
 
   glGetRenderbufferParameteriv: (target, pname, params) => {
@@ -2093,7 +2074,7 @@
   },
 
   glIsRenderbuffer: (renderbuffer) => {
-    var rb = GL.renderbuffers[renderbuffer];
+    var rb = GL.renderbuffers.get(renderbuffer);
     if (!rb) return 0;
     return GLctx.isRenderbuffer(rb);
   },
@@ -2114,10 +2095,10 @@
       return;
     }
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glGetUniform*v', 'program');
-    GL.validateGLObjectID(program.uniformLocsById, location, 'glGetUniform*v', 'location');
+    GL.validateGLHandle(GL.programs, program, 'glGetUniform*v', 'program');
+    GL.validateGL(program.uniformLocsById, location, 'glGetUniform*v', 'location');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
     webglPrepareUniformLocationsBeforeFirstUse(program);
     var data = GLctx.getUniform(program, webglGetUniformLocation(location));
     if (typeof data == 'number' || typeof data == 'boolean') {
@@ -2242,7 +2223,7 @@
   glGetUniformLocation: (program, name) => {
 
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glGetUniformLocation', 'program');
+    GL.validateGLHandle(GL.programs, program, 'glGetUniformLocation', 'program');
 #endif
     name = UTF8ToString(name);
 
@@ -2250,7 +2231,7 @@
     assert(!name.includes(' '), `Uniform names passed to glGetUniformLocation() should not contain spaces! (received "${name}")`);
 #endif
 
-    if (program = GL.programs[program]) {
+    if (program = GL.programs.get(program)) {
       webglPrepareUniformLocationsBeforeFirstUse(program);
       var uniformLocsById = program.uniformLocsById; // Maps GLuint -> WebGLUniformLocation
       var arrayIndex = 0;
@@ -2383,7 +2364,7 @@
   glUniform1f__deps: ['$webglGetUniformLocation'],
   glUniform1f: (location, v0) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform1f', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform1f', 'location');
 #endif
     GLctx.uniform1f(webglGetUniformLocation(location), v0);
   },
@@ -2391,7 +2372,7 @@
   glUniform2f__deps: ['$webglGetUniformLocation'],
   glUniform2f: (location, v0, v1) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform2f', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform2f', 'location');
 #endif
     GLctx.uniform2f(webglGetUniformLocation(location), v0, v1);
   },
@@ -2399,7 +2380,7 @@
   glUniform3f__deps: ['$webglGetUniformLocation'],
   glUniform3f: (location, v0, v1, v2) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform3f', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform3f', 'location');
 #endif
     GLctx.uniform3f(webglGetUniformLocation(location), v0, v1, v2);
   },
@@ -2407,7 +2388,7 @@
   glUniform4f__deps: ['$webglGetUniformLocation'],
   glUniform4f: (location, v0, v1, v2, v3) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform4f', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform4f', 'location');
 #endif
     GLctx.uniform4f(webglGetUniformLocation(location), v0, v1, v2, v3);
   },
@@ -2415,7 +2396,7 @@
   glUniform1i__deps: ['$webglGetUniformLocation'],
   glUniform1i: (location, v0) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform1i', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform1i', 'location');
 #endif
     GLctx.uniform1i(webglGetUniformLocation(location), v0);
   },
@@ -2423,7 +2404,7 @@
   glUniform2i__deps: ['$webglGetUniformLocation'],
   glUniform2i: (location, v0, v1) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform2i', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform2i', 'location');
 #endif
     GLctx.uniform2i(webglGetUniformLocation(location), v0, v1);
   },
@@ -2431,7 +2412,7 @@
   glUniform3i__deps: ['$webglGetUniformLocation'],
   glUniform3i: (location, v0, v1, v2) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform3i', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform3i', 'location');
 #endif
     GLctx.uniform3i(webglGetUniformLocation(location), v0, v1, v2);
   },
@@ -2439,7 +2420,7 @@
   glUniform4i__deps: ['$webglGetUniformLocation'],
   glUniform4i: (location, v0, v1, v2, v3) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform4i', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform4i', 'location');
 #endif
     GLctx.uniform4i(webglGetUniformLocation(location), v0, v1, v2, v3);
   },
@@ -2451,7 +2432,7 @@
   ],
   glUniform1iv: (location, count, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform1iv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform1iv', 'location');
     assert((value & 3) == 0, 'Pointer to integer data passed to glUniform1iv must be aligned to four bytes!');
 #endif
 
@@ -2492,7 +2473,7 @@
   ],
   glUniform2iv: (location, count, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform2iv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform2iv', 'location');
     assert((value & 3) == 0, 'Pointer to integer data passed to glUniform2iv must be aligned to four bytes!');
 #endif
 
@@ -2534,7 +2515,7 @@
   ],
   glUniform3iv: (location, count, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform3iv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform3iv', 'location');
     assert((value & 3) == 0, 'Pointer to integer data passed to glUniform3iv must be aligned to four bytes!');
 #endif
 
@@ -2577,7 +2558,7 @@
   ],
   glUniform4iv: (location, count, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform4iv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform4iv', 'location');
     assert((value & 3) == 0, 'Pointer to integer data passed to glUniform4iv must be aligned to four bytes!');
 #endif
 
@@ -2623,7 +2604,7 @@
   ],
   glUniform1fv: (location, count, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform1fv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform1fv', 'location');
     assert((value & 3) == 0, 'Pointer to float data passed to glUniform1fv must be aligned to four bytes!');
 #endif
 
@@ -2664,7 +2645,7 @@
   ],
   glUniform2fv: (location, count, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform2fv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform2fv', 'location');
     assert((value & 3) == 0, 'Pointer to float data passed to glUniform2fv must be aligned to four bytes!');
 #endif
 
@@ -2708,7 +2689,7 @@
   ],
   glUniform3fv: (location, count, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform3fv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform3fv', 'location');
     assert((value & 3) == 0, 'Pointer to float data passed to glUniform3fv must be aligned to four bytes!' + value);
 #endif
 
@@ -2753,7 +2734,7 @@
   ],
   glUniform4fv: (location, count, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniform4fv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniform4fv', 'location');
     assert((value & 3) == 0, 'Pointer to float data passed to glUniform4fv must be aligned to four bytes!');
 #endif
 
@@ -2803,7 +2784,7 @@
   ],
   glUniformMatrix2fv: (location, count, transpose, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix2fv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix2fv', 'location');
     assert((value & 3) == 0, 'Pointer to float data passed to glUniformMatrix2fv must be aligned to four bytes!');
 #endif
 
@@ -2849,7 +2830,7 @@
   ],
   glUniformMatrix3fv: (location, count, transpose, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix3fv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix3fv', 'location');
     assert((value & 3) == 0, 'Pointer to float data passed to glUniformMatrix3fv must be aligned to four bytes!');
 #endif
 
@@ -2900,7 +2881,7 @@
   ],
   glUniformMatrix4fv: (location, count, transpose, value) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix4fv', 'location');
+    GL.validateGLHandle(GLctx.currentProgram.uniformLocsById, location, 'glUniformMatrix4fv', 'location');
     assert((value & 3) == 0, 'Pointer to float data passed to glUniformMatrix4fv must be aligned to four bytes!');
 #endif
 
@@ -2957,7 +2938,7 @@
 
   glBindBuffer: (target, buffer) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.buffers, buffer, 'glBindBuffer', 'buffer');
+    GL.validateGLHandle(GL.buffers, buffer, 'glBindBuffer', 'buffer');
 #endif
 #if FULL_ES2 || LEGACY_GL_EMULATION
     if (target == 0x8892 /*GL_ARRAY_BUFFER*/) {
@@ -2987,7 +2968,7 @@
       GLctx.currentPixelUnpackBufferBinding = buffer;
     }
 #endif
-    GLctx.bindBuffer(target, GL.buffers[buffer]);
+    GLctx.bindBuffer(target, GL.buffers.get(buffer));
   },
 
   glVertexAttrib1fv: (index, v) => {
@@ -3027,15 +3008,15 @@
   },
 
   glGetAttribLocation: (program, name) => {
-    return GLctx.getAttribLocation(GL.programs[program], UTF8ToString(name));
+    return GLctx.getAttribLocation(GL.programs.get(program), UTF8ToString(name));
   },
 
   $__glGetActiveAttribOrUniform__deps: ['$stringToUTF8'],
   $__glGetActiveAttribOrUniform: (funcName, program, index, bufSize, length, size, type, name) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, funcName, 'program');
+    GL.validateGLHandle(GL.programs, program, funcName, 'program');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
     var info = GLctx[funcName](program, index);
     if (info) {
       // If an error occurs, nothing will be written to length, size and type and name.
@@ -3057,35 +3038,31 @@
   },
 
   glCreateShader: (shaderType) => {
-    var id = GL.getNewId(GL.shaders);
-    GL.shaders[id] = GLctx.createShader(shaderType);
-
+    var shader = GLctx.createShader(shaderType);
 #if GL_EXPLICIT_UNIFORM_LOCATION || GL_EXPLICIT_UNIFORM_BINDING
     // GL_VERTEX_SHADER = 0x8B31, GL_FRAGMENT_SHADER = 0x8B30
-    GL.shaders[id].shaderType = shaderType&1?'vs':'fs';
+    shader.shaderType = shaderType & 1 ? 'vs' : 'fs';
 #endif
-
-    return id;
+    return GL.shaders.allocate(shader);
   },
 
   glDeleteShader: (id) => {
     if (!id) return;
-    var shader = GL.shaders[id];
-    if (!shader) {
-      // glDeleteShader actually signals an error when deleting a nonexisting
-      // object, unlike some other GL delete functions.
+    // glDeleteShader actually signals an error when deleting a nonexisting
+    // object, unlike some other GL delete functions.
+    if (!GL.shaders.has(id)) {
       GL.recordError(0x501 /* GL_INVALID_VALUE */);
       return;
     }
-    GLctx.deleteShader(shader);
-    GL.shaders[id] = null;
+    GLctx.deleteShader(GL.shaders.get(id));
+    GL.shaders.free(id);
   },
 
   glGetAttachedShaders: (program, maxCount, count, shaders) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glGetAttachedShaders', 'program');
+    GL.validateGLHandle(GL.programs, program, 'glGetAttachedShaders', 'program');
 #endif
-    var result = GLctx.getAttachedShaders(GL.programs[program]);
+    var result = GLctx.getAttachedShaders(GL.programs.get(program));
     var len = result.length;
     if (len > maxCount) {
       len = maxCount;
@@ -3105,7 +3082,7 @@
 #endif
   glShaderSource: (shader, count, string, length) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.shaders, shader, 'glShaderSource', 'shader');
+    GL.validateGLHandle(GL.shaders, shader, 'glShaderSource', 'shader');
 #endif
     var source = GL.getSource(shader, count, string, length);
 
@@ -3196,7 +3173,7 @@
     source = source.replace(regex, '$2');
 
     // Remember all the directives to be handled after glLinkProgram is called.
-    GL.shaders[shader].explicitUniformLocations = explicitUniformLocations;
+    GL.shaders.get(shader).explicitUniformLocations = explicitUniformLocations;
 
 #if GL_DEBUG
     dbg(`Shader source after removing layout location directives: ${source}`;
@@ -3268,19 +3245,19 @@
 #endif
 
     // Remember all the directives to be handled after glLinkProgram is called.
-    GL.shaders[shader].explicitSamplerBindings = samplerBindings;
-    GL.shaders[shader].explicitUniformBindings = uniformBindings;
+    GL.shaders.get(shader).explicitSamplerBindings = samplerBindings;
+    GL.shaders.get(shader).explicitUniformBindings = uniformBindings;
 
 #endif // ~GL_EXPLICIT_UNIFORM_BINDING
 
-    GLctx.shaderSource(GL.shaders[shader], source);
+    GLctx.shaderSource(GL.shaders.get(shader), source);
   },
 
   glGetShaderSource: (shader, bufSize, length, source) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.shaders, shader, 'glGetShaderSource', 'shader');
+    GL.validateGLHandle(GL.shaders, shader, 'glGetShaderSource', 'shader');
 #endif
-    var result = GLctx.getShaderSource(GL.shaders[shader]);
+    var result = GLctx.getShaderSource(GL.shaders.get(shader));
     if (!result) return; // If an error occurs, nothing will be written to length or source.
     var numBytesWrittenExclNull = (bufSize > 0 && source) ? stringToUTF8(result, source, bufSize) : 0;
     if (length) {{{ makeSetValue('length', '0', 'numBytesWrittenExclNull', 'i32') }}};
@@ -3288,11 +3265,11 @@
 
   glCompileShader: (shader) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.shaders, shader, 'glCompileShader', 'shader');
+    GL.validateGLHandle(GL.shaders, shader, 'glCompileShader', 'shader');
 #endif
-    GLctx.compileShader(GL.shaders[shader]);
+    GLctx.compileShader(GL.shaders.get(shader));
 #if GL_DEBUG
-    var log = (GLctx.getShaderInfoLog(GL.shaders[shader]) || '').trim();
+    var log = (GLctx.getShaderInfoLog(GL.shaders.get(shader)) || '').trim();
     if (log) dbg(`glCompileShader: ${log}`);
 #endif
   },
@@ -3300,9 +3277,9 @@
   glGetShaderInfoLog__deps: ['$stringToUTF8'],
   glGetShaderInfoLog: (shader, maxLength, length, infoLog) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.shaders, shader, 'glGetShaderInfoLog', 'shader');
+    GL.validateGLHandle(GL.shaders, shader, 'glGetShaderInfoLog', 'shader');
 #endif
-    var log = GLctx.getShaderInfoLog(GL.shaders[shader]);
+    var log = GLctx.getShaderInfoLog(GL.shaders.get(shader));
 #if GL_ASSERTIONS || GL_TRACK_ERRORS
     if (log === null) log = '(unknown error)';
 #endif
@@ -3322,10 +3299,10 @@
       return;
     }
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.shaders, shader, 'glGetShaderiv', 'shader');
+    GL.validateGLHandle(GL.shaders, shader, 'glGetShaderiv', 'shader');
 #endif
     if (pname == 0x8B84) { // GL_INFO_LOG_LENGTH
-      var log = GLctx.getShaderInfoLog(GL.shaders[shader]);
+      var log = GLctx.getShaderInfoLog(GL.shaders.get(shader));
 #if GL_ASSERTIONS || GL_TRACK_ERRORS
       if (log === null) log = '(unknown error)';
 #endif
@@ -3336,13 +3313,13 @@
       var logLength = log ? log.length + 1 : 0;
       {{{ makeSetValue('p', '0', 'logLength', 'i32') }}};
     } else if (pname == 0x8B88) { // GL_SHADER_SOURCE_LENGTH
-      var source = GLctx.getShaderSource(GL.shaders[shader]);
+      var source = GLctx.getShaderSource(GL.shaders.get(shader));
       // source may be a null, or the empty string, both of which are falsey
       // values that we report a 0 length for.
       var sourceLength = source ? source.length + 1 : 0;
       {{{ makeSetValue('p', '0', 'sourceLength', 'i32') }}};
     } else {
-      {{{ makeSetValue('p', '0', 'GLctx.getShaderParameter(GL.shaders[shader], pname)', 'i32') }}};
+      {{{ makeSetValue('p', '0', 'GLctx.getShaderParameter(GL.shaders.get(shader), pname)', 'i32') }}};
     }
   },
 
@@ -3358,10 +3335,10 @@
       return;
     }
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glGetProgramiv', 'program');
+    GL.validateGLHandle(GL.programs, program, 'glGetProgramiv', 'program');
 #endif
 
-    if (program >= GL.counter) {
+    if (GL.programs.has(program)) {
 #if GL_ASSERTIONS
       err(`GL_INVALID_VALUE in glGetProgramiv(program=${program}, pname=${pname}, p=${ptrToString(p)}): The specified program object name was not generated by GL!`);
 #endif
@@ -3369,7 +3346,7 @@
       return;
     }
 
-    program = GL.programs[program];
+    program = GL.programs.get(program);
 
     if (pname == 0x8B84) { // GL_INFO_LOG_LENGTH
       var log = GLctx.getProgramInfoLog(program);
@@ -3404,59 +3381,56 @@
   },
 
   glIsShader: (shader) => {
-    var s = GL.shaders[shader];
-    if (!s) return 0;
+    if (!GL.shaders.has(shader)) return 0;
+    var s = GL.shaders.get(shader);
     return GLctx.isShader(s);
   },
 
   glCreateProgram: () => {
-    var id = GL.getNewId(GL.programs);
     var program = GLctx.createProgram();
     // Store additional information needed for each shader program:
-    program.name = id;
-    // Lazy cache results of
-    // glGetProgramiv(GL_ACTIVE_UNIFORM_MAX_LENGTH/GL_ACTIVE_ATTRIBUTE_MAX_LENGTH/GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH)
+    program.name = GL.programs.allocate(program);
+    // Lazy cache results of glGetProgramiv(GL_ACTIVE_UNIFORM_MAX_LENGTH/GL_ACTIVE_ATTRIBUTE_MAX_LENGTH/GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH)
     program.maxUniformLength = program.maxAttributeLength = program.maxUniformBlockNameLength = 0;
     program.uniformIdCounter = 1;
-    GL.programs[id] = program;
-    return id;
+    return program.name;
   },
 
   glDeleteProgram: (id) => {
     if (!id) return;
-    var program = GL.programs[id];
+    var program = GL.programs.get(id);
+    // glDeleteProgram actually signals an error when deleting a nonexisting
+    // object, unlike some other GL delete functions.
     if (!program) {
-      // glDeleteProgram actually signals an error when deleting a nonexisting
-      // object, unlike some other GL delete functions.
       GL.recordError(0x501 /* GL_INVALID_VALUE */);
       return;
     }
     GLctx.deleteProgram(program);
     program.name = 0;
-    GL.programs[id] = null;
+    GL.programs.free(id);
   },
 
   glAttachShader: (program, shader) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glAttachShader', 'program');
-    GL.validateGLObjectID(GL.shaders, shader, 'glAttachShader', 'shader');
+    GL.validateGLHandle(GL.programs, program, 'glAttachShader', 'program');
+    GL.validateGLHandle(GL.shaders, shader, 'glAttachShader', 'shader');
 #endif
 #if GL_EXPLICIT_UNIFORM_LOCATION || GL_EXPLICIT_UNIFORM_BINDING
-    program = GL.programs[program];
-    shader = GL.shaders[shader];
+    program = GL.programs.get(program);
+    shader = GL.shaders.get(shader);
     program[shader.shaderType] = shader;
     GLctx.attachShader(program, shader);
 #else
-    GLctx.attachShader(GL.programs[program], GL.shaders[shader]);
+    GLctx.attachShader(GL.programs.get(program), GL.shaders.get(shader));
 #endif
   },
 
   glDetachShader: (program, shader) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glDetachShader', 'program');
-    GL.validateGLObjectID(GL.shaders, shader, 'glDetachShader', 'shader');
+    GL.validateGLHandle(GL.programs, program, 'glDetachShader', 'program');
+    GL.validateGLHandle(GL.shaders, shader, 'glDetachShader', 'shader');
 #endif
-    GLctx.detachShader(GL.programs[program], GL.shaders[shader]);
+    GLctx.detachShader(GL.programs.get(program), GL.shaders.get(shader));
   },
 
   glGetShaderPrecisionFormat: (shaderType, precisionType, range, precision) => {
@@ -3468,9 +3442,9 @@
 
   glLinkProgram: (program) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glLinkProgram', 'program');
+    GL.validateGLHandle(GL.programs, program, 'glLinkProgram', 'program');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
     GLctx.linkProgram(program);
 #if GL_DEBUG
     var log = (GLctx.getProgramInfoLog(program) || '').trim();
@@ -3520,9 +3494,9 @@
 
   glGetProgramInfoLog: (program, maxLength, length, infoLog) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glGetProgramInfoLog', 'program');
+    GL.validateGLHandle(GL.programs, program, 'glGetProgramInfoLog', 'program');
 #endif
-    var log = GLctx.getProgramInfoLog(GL.programs[program]);
+    var log = GLctx.getProgramInfoLog(GL.programs.get(program));
 #if GL_ASSERTIONS || GL_TRACK_ERRORS
     if (log === null) log = '(unknown error)';
 #endif
@@ -3573,9 +3547,9 @@
 #endif
   glUseProgram: (program) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glUseProgram', 'program');
+    GL.validateGLHandle(GL.programs, program, 'glUseProgram', 'program');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
     GLctx.useProgram(program);
     // Record the currently active program so that we can access the uniform
     // mapping table of that program.
@@ -3590,36 +3564,36 @@
 
   glValidateProgram: (program) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glValidateProgram', 'program');
+    GL.validateGLHandle(GL.programs, program, 'glValidateProgram', 'program');
 #endif
-    GLctx.validateProgram(GL.programs[program]);
+    GLctx.validateProgram(GL.programs.get(program));
   },
 
   glIsProgram: (program) => {
-    program = GL.programs[program];
-    if (!program) return 0;
+    if (!GL.programs.has(program)) return 0;
+    program = GL.programs.get(program);
     return GLctx.isProgram(program);
   },
 
   glBindAttribLocation: (program, index, name) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.programs, program, 'glBindAttribLocation', 'program');
+    GL.validateGLHandle(GL.programs, program, 'glBindAttribLocation', 'program');
 #endif
-    GLctx.bindAttribLocation(GL.programs[program], index, UTF8ToString(name));
+    GLctx.bindAttribLocation(GL.programs.get(program), index, UTF8ToString(name));
   },
 
   glBindFramebuffer: (target, framebuffer) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.framebuffers, framebuffer, 'glBindFramebuffer', 'framebuffer');
+    GL.validateGLHandle(GL.framebuffers, framebuffer, 'glBindFramebuffer', 'framebuffer');
 #endif
 
 #if OFFSCREEN_FRAMEBUFFER
     // defaultFbo may not be present if 'renderViaOffscreenBackBuffer' was not enabled during context creation time,
     // i.e. setting -sOFFSCREEN_FRAMEBUFFER at compilation time does not yet mandate that offscreen back buffer
     // is being used, but that is ultimately decided at context creation time.
-    GLctx.bindFramebuffer(target, framebuffer ? GL.framebuffers[framebuffer] : GL.currentContext.defaultFbo);
+    GLctx.bindFramebuffer(target, framebuffer ? GL.framebuffers.get(framebuffer) : GL.currentContext.defaultFbo);
 #else
-    GLctx.bindFramebuffer(target, GL.framebuffers[framebuffer]);
+    GLctx.bindFramebuffer(target, GL.framebuffers.get(framebuffer));
 #endif
 
   },
@@ -3635,28 +3609,28 @@
   glDeleteFramebuffers: (n, framebuffers) => {
     for (var i = 0; i < n; ++i) {
       var id = {{{ makeGetValue('framebuffers', 'i*4', 'i32') }}};
-      var framebuffer = GL.framebuffers[id];
+      var framebuffer = GL.framebuffers.get(id);
       if (!framebuffer) continue; // GL spec: "glDeleteFramebuffers silently ignores 0s and names that do not correspond to existing framebuffer objects".
       GLctx.deleteFramebuffer(framebuffer);
       framebuffer.name = 0;
-      GL.framebuffers[id] = null;
+      GL.framebuffers.free(id);
     }
   },
 
   glFramebufferRenderbuffer: (target, attachment, renderbuffertarget, renderbuffer) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.renderbuffers, renderbuffer, 'glFramebufferRenderbuffer', 'renderbuffer');
+    GL.validateGLHandle(GL.renderbuffers, renderbuffer, 'glFramebufferRenderbuffer', 'renderbuffer');
 #endif
     GLctx.framebufferRenderbuffer(target, attachment, renderbuffertarget,
-                                       GL.renderbuffers[renderbuffer]);
+                                       GL.renderbuffers.get(renderbuffer));
   },
 
   glFramebufferTexture2D: (target, attachment, textarget, texture, level) => {
 #if GL_ASSERTIONS
-    GL.validateGLObjectID(GL.textures, texture, 'glFramebufferTexture2D', 'texture');
+    GL.validateGLHandle(GL.textures, texture, 'glFramebufferTexture2D', 'texture');
 #endif
     GLctx.framebufferTexture2D(target, attachment, textarget,
-                                    GL.textures[texture], level);
+                                    GL.textures.get(texture), level);
   },
 
   glGetFramebufferAttachmentParameteriv: (target, attachment, pname, params) => {
@@ -3669,8 +3643,8 @@
   },
 
   glIsFramebuffer: (framebuffer) => {
-    var fb = GL.framebuffers[framebuffer];
-    if (!fb) return 0;
+    if (!GL.framebuffers.has(framebuffer)) return 0;
+    var fb = GL.framebuffers.get(framebuffer);
     return GLctx.isFramebuffer(fb);
   },
 
diff --git a/src/library_webgl2.js b/src/library_webgl2.js
index 269b187..3b7c6c7 100644
--- a/src/library_webgl2.js
+++ b/src/library_webgl2.js
@@ -370,7 +370,7 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.programs, program, 'glTransformFeedbackVaryings', 'program');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
     var vars = [];
     for (var i = 0; i < count; i++)
       vars.push(UTF8ToString({{{ makeGetValue('varyings', 'i*4', 'i32') }}}));
@@ -382,7 +382,7 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.programs, program, 'glGetTransformFeedbackVarying', 'program');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
     var info = GLctx.getTransformFeedbackVarying(program, index);
     if (!info) return; // If an error occurred, the return parameters length, size, type and name will be unmodified.
 
@@ -474,14 +474,14 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.buffers, buffer, 'glBindBufferBase', 'buffer');
 #endif
-    GLctx.bindBufferBase(target, index, GL.buffers[buffer]);
+    GLctx.bindBufferBase(target, index, GL.buffers.get(buffer));
   },
 
   glBindBufferRange: (target, index, buffer, offset, ptrsize) => {
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.buffers, buffer, 'glBindBufferRange', 'buffer');
 #endif
-    GLctx.bindBufferRange(target, index, GL.buffers[buffer], offset, ptrsize);
+    GLctx.bindBufferRange(target, index, GL.buffers.get(buffer), offset, ptrsize);
   },
 
   glGetUniformIndices: (program, uniformCount, uniformNames, uniformIndices) => {
@@ -503,7 +503,7 @@
       return;
     }
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
     var names = [];
     for (var i = 0; i < uniformCount; i++)
       names.push(UTF8ToString({{{ makeGetValue('uniformNames', 'i*4', 'i32') }}}));
@@ -536,7 +536,7 @@
       return;
     }
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
     var ids = [];
     for (var i = 0; i < uniformCount; i++) {
       ids.push({{{ makeGetValue('uniformIndices', 'i*4', 'i32') }}});
@@ -555,7 +555,7 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.programs, program, 'glGetUniformBlockIndex', 'program');
 #endif
-    return GLctx.getUniformBlockIndex(GL.programs[program], UTF8ToString(uniformBlockName));
+    return GLctx.getUniformBlockIndex(GL.programs.get(program), UTF8ToString(uniformBlockName));
   },
 
   glGetActiveUniformBlockiv: (program, uniformBlockIndex, pname, params) => {
@@ -573,7 +573,7 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.programs, program, 'glGetActiveUniformBlockiv', 'program');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
 
     if (pname == 0x8A41 /* GL_UNIFORM_BLOCK_NAME_LENGTH */) {
       var name = GLctx.getActiveUniformBlockName(program, uniformBlockIndex);
@@ -596,7 +596,7 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.programs, program, 'glGetActiveUniformBlockName', 'program');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
 
     var result = GLctx.getActiveUniformBlockName(program, uniformBlockIndex);
     if (!result) return; // If an error occurs, nothing will be written to uniformBlockName or length.
@@ -612,7 +612,7 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.programs, program, 'glUniformBlockBinding', 'program');
 #endif
-    program = GL.programs[program];
+    program = GL.programs.get(program);
 
     GLctx.uniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
   },
@@ -643,13 +643,10 @@
 
   glFenceSync: (condition, flags) => {
     var sync = GLctx.fenceSync(condition, flags);
-    if (sync) {
-      var id = GL.getNewId(GL.syncs);
-      sync.name = id;
-      GL.syncs[id] = sync;
-      return id;
+    if (!sync) {
+      return 0; // Failed to create a sync object
     }
-    return 0; // Failed to create a sync object
+    return sync.name = GL.syncs.allocate(sync);
   },
 
   glDeleteSync: (id) => {
@@ -723,7 +720,7 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.programs, program, 'glGetFragDataLocation', 'program');
 #endif
-    return GLctx.getFragDataLocation(GL.programs[program], UTF8ToString(name));
+    return GLctx.getFragDataLocation(GL.programs.get(program), UTF8ToString(name));
   },
 
   glGetVertexAttribIiv__deps: ['$emscriptenWebGLGetVertexAttrib'],
@@ -901,7 +898,7 @@
 #if GL_ASSERTIONS
     GL.validateGLObjectID(GL.textures, texture, 'glFramebufferTextureLayer', 'texture');
 #endif
-    GLctx.framebufferTextureLayer(target, attachment, GL.textures[texture], level, layer);
+    GLctx.framebufferTextureLayer(target, attachment, GL.textures.get(texture), level, layer);
   },
 
   glVertexAttribIPointer: (index, size, type, stride, ptr) => {
diff --git a/test/code_size/hello_webgl2_wasm.json b/test/code_size/hello_webgl2_wasm.json
index f7caf6b..bffc225 100644
--- a/test/code_size/hello_webgl2_wasm.json
+++ b/test/code_size/hello_webgl2_wasm.json
@@ -1,10 +1,10 @@
 {
   "a.html": 569,
   "a.html.gz": 379,
-  "a.js": 4589,
-  "a.js.gz": 2341,
+  "a.js": 4726,
+  "a.js.gz": 2381,
   "a.wasm": 10451,
   "a.wasm.gz": 6724,
-  "total": 15609,
-  "total_gz": 9444
+  "total": 15746,
+  "total_gz": 9484
 }
diff --git a/test/code_size/hello_webgl2_wasm2js.json b/test/code_size/hello_webgl2_wasm2js.json
index e110956..a294ff0 100644
--- a/test/code_size/hello_webgl2_wasm2js.json
+++ b/test/code_size/hello_webgl2_wasm2js.json
@@ -1,10 +1,10 @@
 {
   "a.html": 567,
   "a.html.gz": 379,
-  "a.js": 17795,
-  "a.js.gz": 7979,
+  "a.js": 17935,
+  "a.js.gz": 8021,
   "a.mem": 3123,
   "a.mem.gz": 2693,
-  "total": 21485,
-  "total_gz": 11051
+  "total": 21625,
+  "total_gz": 11093
 }
diff --git a/test/code_size/hello_webgl_wasm.json b/test/code_size/hello_webgl_wasm.json
index 9b2af9a..c7b02d4 100644
--- a/test/code_size/hello_webgl_wasm.json
+++ b/test/code_size/hello_webgl_wasm.json
@@ -1,10 +1,10 @@
 {
   "a.html": 569,
   "a.html.gz": 379,
-  "a.js": 4075,
-  "a.js.gz": 2170,
+  "a.js": 4212,
+  "a.js.gz": 2216,
   "a.wasm": 10451,
   "a.wasm.gz": 6724,
-  "total": 15095,
-  "total_gz": 9273
+  "total": 15232,
+  "total_gz": 9319
 }
diff --git a/test/code_size/hello_webgl_wasm2js.json b/test/code_size/hello_webgl_wasm2js.json
index 6b0dba3..bcbc837 100644
--- a/test/code_size/hello_webgl_wasm2js.json
+++ b/test/code_size/hello_webgl_wasm2js.json
@@ -1,10 +1,10 @@
 {
   "a.html": 567,
   "a.html.gz": 379,
-  "a.js": 17267,
-  "a.js.gz": 7812,
+  "a.js": 17407,
+  "a.js.gz": 7848,
   "a.mem": 3123,
   "a.mem.gz": 2693,
-  "total": 20957,
-  "total_gz": 10884
+  "total": 21097,
+  "total_gz": 10920
 }
diff --git a/test/code_size/math_wasm.json b/test/code_size/math_wasm.json
index e504038..d682fcd 100644
--- a/test/code_size/math_wasm.json
+++ b/test/code_size/math_wasm.json
@@ -3,8 +3,8 @@
   "a.html.gz": 431,
   "a.js": 110,
   "a.js.gz": 125,
-  "a.wasm": 2728,
-  "a.wasm.gz": 1672,
-  "total": 3511,
-  "total_gz": 2228
+  "a.wasm": 2724,
+  "a.wasm.gz": 1667,
+  "total": 3507,
+  "total_gz": 2223
 }
diff --git a/test/minimal_webgl/library_js.js b/test/minimal_webgl/library_js.js
index b546b99..92dfc62 100644
--- a/test/minimal_webgl/library_js.js
+++ b/test/minimal_webgl/library_js.js
@@ -34,7 +34,7 @@
     img.onload = () => {
       HEAPU32[outW>>2] = img.width;
       HEAPU32[outH>>2] = img.height;
-      GLctx.bindTexture(0xDE1/*GLctx.TEXTURE_2D*/, GL.textures[glTexture]);
+      GLctx.bindTexture(0xDE1/*GLctx.TEXTURE_2D*/, GL.textures.get(glTexture));
       _uploadFlipped(img);
     };
     img.src = UTF8ToString(url);