Repository: bytecodealliance/wasmtime
Pull request #8550 (2024-05-04 15:48:46)

wasmtime(gc): Add the `wasmtime::NoExtern` value type
<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->


github-actions[bot]: #### Subscribe to Label Action

cc @fitzgen

<details>
This issue or pull request has been labeled: "wasmtime:api", "wasmtime:ref-types"

Thus the following users have been cc'd because of the following labels:

* fitzgen: wasmtime:ref-types

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>

Patch:
diff --git a/cranelift/codegen/src/isa/x64/inst/emit.rs b/cranelift/codegen/src/isa/x64/inst/emit.rs
index f1efd2151..d978caefc 100644
--- a/cranelift/codegen/src/isa/x64/inst/emit.rs
+++ b/cranelift/codegen/src/isa/x64/inst/emit.rs
@@ -28,10 +28,11 @@ fn emit_signed_cvt(
     } else {
         SseOpcode::Cvtsi2ss
     };
+    let dst = WritableXmm::from_writable_reg(dst).unwrap();
     Inst::CvtIntToFloat {
         op,
-        dst: Writable::from_reg(Xmm::new(dst.to_reg()).unwrap()),
-        src1: Xmm::new(dst.to_reg()).unwrap(),
+        dst,
+        src1: dst.to_reg(),
         src2: GprMem::new(RegMem::reg(src)).unwrap(),
         src2_size: OperandSize::Size64,
     }
@@ -235,15 +236,16 @@ pub(crate) fn emit(
             }
         }
 
-        Inst::AluConstOp { op, size, dst } => {
-            let dst = allocs.next(dst.to_reg().to_reg());
+        &Inst::AluConstOp { op, size, dst } => {
+            let dst = WritableGpr::from_writable_reg(allocs.next_writable(dst.to_writable_reg()))
+                .unwrap();
             emit(
                 &Inst::AluRmiR {
-                    size: *size,
-                    op: *op,
-                    dst: Writable::from_reg(Gpr::new(dst).unwrap()),
-                    src1: Gpr::new(dst).unwrap(),
-                    src2: Gpr::new(dst).unwrap().into(),
+                    size,
+                    op,
+                    dst,
+                    src1: dst.to_reg(),
+                    src2: dst.to_reg().into(),
                 },
                 allocs,
                 sink,
@@ -890,8 +892,8 @@ pub(crate) fn emit(
             debug_assert!([regs::rsp(), regs::rbp(), regs::pinned_reg()].contains(&src));
             let src = Gpr::new(src).unwrap();
             let size = OperandSize::Size64;
-            let dst = allocs.next(dst.to_reg().to_reg());
-            let dst = WritableGpr::from_writable_reg(Writable::from_reg(dst)).unwrap();
+            let dst = WritableGpr::from_writable_reg(allocs.next_writable(dst.to_writable_reg()))
+                .unwrap();
             Inst::MovRR { size, src, dst }.emit(&[], sink, info, state);
         }
 
@@ -1426,8 +1428,8 @@ pub(crate) fn emit(
             dst,
         } => {
             let alternative = allocs.next(alternative.to_reg());
-            let dst = allocs.next(dst.to_reg().to_reg());
-            debug_assert_eq!(alternative, dst);
+            let dst = allocs.next_writable(dst.to_writable_reg());
+            debug_assert_eq!(alternative, dst.to_reg());
             let consequent = allocs.next(consequent.clone().to_reg());
 
             // Lowering of the Select IR opcode when the input is an fcmp relies on the fact that
@@ -1447,7 +1449,7 @@ pub(crate) fn emit(
                     SseOpcode::Movdqa
                 }
             };
-            let inst = Inst::xmm_unary_rm_r(op, consequent.into(), Writable::from_reg(dst));
+            let inst = Inst::xmm_unary_rm_r(op, consequent.into(), dst);
             inst.emit(&[], sink, info, state);
 
             sink.bind_label(next, state.ctrl_plane_mut());
@@ -1806,17 +1808,17 @@ pub(crate) fn emit(
             }
         }
 
-        Inst::JmpTableSeq {
+        &Inst::JmpTableSeq {
             idx,
             tmp1,
             tmp2,
             ref targets,
-            default_target,
+            ref default_target,
             ..
         } => {
-            let idx = allocs.next(*idx);
-            let tmp1 = Writable::from_reg(allocs.next(tmp1.to_reg()));
-            let tmp2 = Writable::from_reg(allocs.next(tmp2.to_reg()));
+            let idx = allocs.next(idx);
+            let tmp1 = allocs.next_writable(tmp1);
+            let tmp2 = allocs.next_writable(tmp2);
 
             // This sequence is *one* instruction in the vcode, and is expanded only here at
             // emission time, because we cannot allow the regalloc to insert spills/reloads in
@@ -2941,8 +2943,8 @@ pub(crate) fn emit(
         } => {
             let rhs = allocs.next(rhs.to_reg());
             let lhs = allocs.next(lhs.to_reg());
-            let dst = allocs.next(dst.to_reg().to_reg());
-            debug_assert_eq!(rhs, dst);
+            let dst = allocs.next_writable(dst.to_writable_reg());
+            debug_assert_eq!(rhs, dst.to_reg());
 
             // Generates the following sequence:
             // cmpss/cmpsd %lhs, %rhs_dst
@@ -2993,7 +2995,7 @@ pub(crate) fn emit(
                 _ => unreachable!(),
             };
 
-            let inst = Inst::xmm_cmp_rm_r(cmp_op, dst, RegMem::reg(lhs));
+            let inst = Inst::xmm_cmp_rm_r(cmp_op, dst.to_reg(), RegMem::reg(lhs));
             inst.emit(&[], sink, info, state);
 
             one_way_jmp(sink, CC::NZ, do_min_max);
@@ -3003,7 +3005,7 @@ pub(crate) fn emit(
             // and negative zero. These instructions merge the sign bits in that
             // case, and are no-ops otherwise.
             let op = if *is_min { or_op } else { and_op };
-            let inst = Inst::xmm_rm_r(op, RegMem::reg(lhs), Writable::from_reg(dst));
+            let inst = Inst::xmm_rm_r(op, RegMem::reg(lhs), dst);
             inst.emit(&[], sink, info, state);
 
             let inst = Inst::jmp_known(done);
@@ -3013,14 +3015,14 @@ pub(crate) fn emit(
             // read-only operand: perform an addition between the two operands, which has the
             // desired NaN propagation effects.
             sink.bind_label(propagate_nan, state.ctrl_plane_mut());
-            let inst = Inst::xmm_rm_r(add_op, RegMem::reg(lhs), Writable::from_reg(dst));
+            let inst = Inst::xmm_rm_r(add_op, RegMem::reg(lhs), dst);
             inst.emit(&[], sink, info, state);
 
             one_way_jmp(sink, CC::P, done);
 
             sink.bind_label(do_min_max, state.ctrl_plane_mut());
 
-            let inst = Inst::xmm_rm_r(min_max_op, RegMem::reg(lhs), Writable::from_reg(dst));
+            let inst = Inst::xmm_rm_r(min_max_op, RegMem::reg(lhs), dst);
             inst.emit(&[], sink, info, state);
 
             sink.bind_label(done, state.ctrl_plane_mut());
@@ -3297,9 +3299,9 @@ pub(crate) fn emit(
             tmp_gpr2,
         } => {
             let src = allocs.next(src.to_reg());
-            let dst = allocs.next(dst.to_reg().to_reg());
-            let tmp_gpr1 = allocs.next(tmp_gpr1.to_reg().to_reg());
-            let tmp_gpr2 = allocs.next(tmp_gpr2.to_reg().to_reg());
+            let dst = allocs.next_writable(dst.to_writable_reg());
+            let tmp_gpr1 = allocs.next_writable(tmp_gpr1.to_writable_reg());
+            let tmp_gpr2 = allocs.next_writable(tmp_gpr2.to_writable_reg());
 
             // Note: this sequence is specific to 64-bit mode; a 32-bit mode would require a
             // different sequence.
@@ -3325,9 +3327,8 @@ pub(crate) fn emit(
             //
             //  done:
 
-            assert_ne!(src, tmp_gpr1);
-            assert_ne!(src, tmp_gpr2);
-            assert_ne!(tmp_gpr1, tmp_gpr2);
+            assert_ne!(src, tmp_gpr1.to_reg());
+            assert_ne!(src, tmp_gpr2.to_reg());
 
             let handle_negative = sink.get_label();
             let done = sink.get_label();
@@ -3347,7 +3348,7 @@ pub(crate) fn emit(
                 info,
                 state,
                 src,
-                Writable::from_reg(dst),
+                dst,
                 *dst_size == OperandSize::Size64,
             );
 
@@ -3358,7 +3359,7 @@ pub(crate) fn emit(
 
             // Divide x by two to get it in range for the signed conversion, keep the LSB, and
             // scale it back up on the FP side.
-            let inst = Inst::gen_move(Writable::from_reg(tmp_gpr1), src, types::I64);
+            let inst = Inst::gen_move(tmp_gpr1, src, types::I64);
             inst.emit(&[], sink, info, state);
 
             // tmp_gpr1 := src >> 1
@@ -3366,27 +3367,27 @@ pub(crate) fn emit(
                 OperandSize::Size64,
                 ShiftKind::ShiftRightLogical,
                 Imm8Gpr::new(Imm8Reg::Imm8 { imm: 1 }).unwrap(),
+                tmp_gpr1.to_reg(),
                 tmp_gpr1,
-                Writable::from_reg(tmp_gpr1),
             );
             inst.emit(&[], sink, info, state);
 
-            let inst = Inst::gen_move(Writable::from_reg(tmp_gpr2), src, types::I64);
+            let inst = Inst::gen_move(tmp_gpr2, src, types::I64);
             inst.emit(&[], sink, info, state);
 
             let inst = Inst::alu_rmi_r(
                 OperandSize::Size64,
                 AluRmiROpcode::And,
                 RegMemImm::imm(1),
-                Writable::from_reg(tmp_gpr2),
+                tmp_gpr2,
             );
             inst.emit(&[], sink, info, state);
 
             let inst = Inst::alu_rmi_r(
                 OperandSize::Size64,
                 AluRmiROpcode::Or,
-                RegMemImm::reg(tmp_gpr1),
-                Writable::from_reg(tmp_gpr2),
+                RegMemImm::reg(tmp_gpr1.to_reg()),
+                tmp_gpr2,
             );
             inst.emit(&[], sink, info, state);
 
@@ -3394,8 +3395,8 @@ pub(crate) fn emit(
                 sink,
                 info,
                 state,
-                tmp_gpr2,
-                Writable::from_reg(dst),
+                tmp_gpr2.to_reg(),
+                dst,
                 *dst_size == OperandSize::Size64,
             );
 
@@ -3404,7 +3405,7 @@ pub(crate) fn emit(
             } else {
                 SseOpcode::Addss
             };
-            let inst = Inst::xmm_rm_r(add_op, RegMem::reg(dst), Writable::from_reg(dst));
+            let inst = Inst::xmm_rm_r(add_op, RegMem::reg(dst.to_reg()), dst);
             inst.emit(&[], sink, info, state);
 
             sink.bind_label(done, state.ctrl_plane_mut());
@@ -3420,9 +3421,9 @@ pub(crate) fn emit(
             tmp_xmm,
         } => {
             let src = allocs.next(src.to_reg());
-            let dst = allocs.next(dst.to_reg().to_reg());
-            let tmp_gpr = allocs.next(tmp_gpr.to_reg().to_reg());
-            let tmp_xmm = allocs.next(tmp_xmm.to_reg().to_reg());
+            let dst = allocs.next_writable(dst.to_writable_reg());
+            let tmp_gpr = allocs.next_writable(tmp_gpr.to_writable_reg());
+            let tmp_xmm = allocs.next_writable(tmp_xmm.to_writable_reg());
 
             // Emits the following common sequence:
             //
@@ -3479,11 +3480,11 @@ pub(crate) fn emit(
             let done = sink.get_label();
 
             // The truncation.
-            let inst = Inst::xmm_to_gpr(trunc_op, src, Writable::from_reg(dst), *dst_size);
+            let inst = Inst::xmm_to_gpr(trunc_op, src, dst, *dst_size);
             inst.emit(&[], sink, info, state);
 
             // Compare against 1, in case of overflow the dst operand was INT_MIN.
-            let inst = Inst::cmp_rmi_r(*dst_size, dst, RegMemImm::imm(1));
+            let inst = Inst::cmp_rmi_r(*dst_size, dst.to_reg(), RegMemImm::imm(1));
             inst.emit(&[], sink, info, state);
 
             one_way_jmp(sink, CC::NO, done); // no overflow => done
@@ -3501,8 +3502,8 @@ pub(crate) fn emit(
                 let inst = Inst::alu_rmi_r(
                     *dst_size,
                     AluRmiROpcode::Xor,
-                    RegMemImm::reg(dst),
-                    Writable::from_reg(dst),
+                    RegMemImm::reg(dst.to_reg()),
+                    dst,
                 );
                 inst.emit(&[], sink, info, state);
 
@@ -3514,14 +3515,10 @@ pub(crate) fn emit(
                 // If the input was positive, saturate to INT_MAX.
 
                 // Zero out tmp_xmm.
-                let inst = Inst::xmm_rm_r(
-                    SseOpcode::Xorpd,
-                    RegMem::reg(tmp_xmm),
-                    Writable::from_reg(tmp_xmm),
-                );
+                let inst = Inst::xmm_rm_r(SseOpcode::Xorpd, RegMem::reg(tmp_xmm.to_reg()), tmp_xmm);
                 inst.emit(&[], sink, info, state);
 
-                let inst = Inst::xmm_cmp_rm_r(cmp_op, tmp_xmm, RegMem::reg(src));
+                let inst = Inst::xmm_cmp_rm_r(cmp_op, tmp_xmm.to_reg(), RegMem::reg(src));
                 inst.emit(&[], sink, info, state);
 
                 // Jump if >= to done.
@@ -3529,14 +3526,10 @@ pub(crate) fn emit(
 
                 // Otherwise, put INT_MAX.
                 if *dst_size == OperandSize::Size64 {
-                    let inst = Inst::imm(
-                        OperandSize::Size64,
-                        0x7fffffffffffffff,
-                        Writable::from_reg(dst),
-                    );
+                    let inst = Inst::imm(OperandSize::Size64, 0x7fffffffffffffff, dst);
                     inst.emit(&[], sink, info, state);
                 } else {
-                    let inst = Inst::imm(OperandSize::Size32, 0x7fffffff, Writable::from_reg(dst));
+                    let inst = Inst::imm(OperandSize::Size32, 0x7fffffff, dst);
                     inst.emit(&[], sink, info, state);
                 }
             } else {
@@ -3554,8 +3547,7 @@ pub(crate) fn emit(
                 match *src_size {
                     OperandSize::Size32 => {
                         let cst = Ieee32::pow2(output_bits - 1).neg().bits();
-                        let inst =
-                            Inst::imm(OperandSize::Size32, cst as u64, Writable::from_reg(tmp_gpr));
+                        let inst = Inst::imm(OperandSize::Size32, cst as u64, tmp_gpr);
                         inst.emit(&[], sink, info, state);
                     }
                     OperandSize::Size64 => {
@@ -3567,22 +3559,17 @@ pub(crate) fn emit(
                         } else {
                             Ieee64::pow2(output_bits - 1).neg()
                         };
-                        let inst =
-                            Inst::imm(OperandSize::Size64, cst.bits(), Writable::from_reg(tmp_gpr));
+                        let inst = Inst::imm(OperandSize::Size64, cst.bits(), tmp_gpr);
                         inst.emit(&[], sink, info, state);
                     }
                     _ => unreachable!(),
                 }
 
-                let inst = Inst::gpr_to_xmm(
-                    cast_op,
-                    RegMem::reg(tmp_gpr),
-                    *src_size,
-                    Writable::from_reg(tmp_xmm),
-                );
+                let inst =
+                    Inst::gpr_to_xmm(cast_op, RegMem::reg(tmp_gpr.to_reg()), *src_size, tmp_xmm);
                 inst.emit(&[], sink, info, state);
 
-                let inst = Inst::xmm_cmp_rm_r(cmp_op, src, RegMem::reg(tmp_xmm));
+                let inst = Inst::xmm_cmp_rm_r(cmp_op, src, RegMem::reg(tmp_xmm.to_reg()));
                 inst.emit(&[], sink, info, state);
 
                 // no trap if src >= or > threshold
@@ -3592,14 +3579,10 @@ pub(crate) fn emit(
                 // If positive, it was a real overflow.
 
                 // Zero out the tmp_xmm register.
-                let inst = Inst::xmm_rm_r(
-                    SseOpcode::Xorpd,
-                    RegMem::reg(tmp_xmm),
-                    Writable::from_reg(tmp_xmm),
-                );
+                let inst = Inst::xmm_rm_r(SseOpcode::Xorpd, RegMem::reg(tmp_xmm.to_reg()), tmp_xmm);
                 inst.emit(&[], sink, info, state);
 
-                let inst = Inst::xmm_cmp_rm_r(cmp_op, tmp_xmm, RegMem::reg(src));
+                let inst = Inst::xmm_cmp_rm_r(cmp_op, tmp_xmm.to_reg(), RegMem::reg(src));
                 inst.emit(&[], sink, info, state);
 
                 // no trap if 0 >= src
@@ -3621,10 +3604,10 @@ pub(crate) fn emit(
             tmp_xmm2,
         } => {
             let src = allocs.next(src.to_reg());
-            let dst = allocs.next(dst.to_reg().to_reg());
-            let tmp_gpr = allocs.next(tmp_gpr.to_reg().to_reg());
-            let tmp_xmm = allocs.next(tmp_xmm.to_reg().to_reg());
-            let tmp_xmm2 = allocs.next(tmp_xmm2.to_reg().to_reg());
+            let dst = allocs.next_writable(dst.to_writable_reg());
+            let tmp_gpr = allocs.next_writable(tmp_gpr.to_writable_reg());
+            let tmp_xmm = allocs.next_writable(tmp_xmm.to_writable_reg());
+            let tmp_xmm2 = allocs.next_writable(tmp_xmm2.to_writable_reg());
 
             // The only difference in behavior between saturating and non-saturating is how we
             // handle errors. Emits the following sequence:
@@ -3660,7 +3643,7 @@ pub(crate) fn emit(
             //
             // done:
 
-            assert_ne!(tmp_xmm, src, "tmp_xmm clobbers src!");
+            assert_ne!(tmp_xmm.to_reg(), src, "tmp_xmm clobbers src!");
 
             let (sub_op, cast_op, cmp_op, trunc_op) = match src_size {
                 OperandSize::Size32 => (
@@ -3686,18 +3669,13 @@ pub(crate) fn emit(
                 _ => unreachable!(),
             };
 
-            let inst = Inst::imm(*src_size, cst, Writable::from_reg(tmp_gpr));
+            let inst = Inst::imm(*src_size, cst, tmp_gpr);
             inst.emit(&[], sink, info, state);
 
-            let inst = Inst::gpr_to_xmm(
-                cast_op,
-                RegMem::reg(tmp_gpr),
-                *src_size,
-                Writable::from_reg(tmp_xmm),
-            );
+            let inst = Inst::gpr_to_xmm(cast_op, RegMem::reg(tmp_gpr.to_reg()), *src_size, tmp_xmm);
             inst.emit(&[], sink, info, state);
 
-            let inst = Inst::xmm_cmp_rm_r(cmp_op, src, RegMem::reg(tmp_xmm));
+            let inst = Inst::xmm_cmp_rm_r(cmp_op, src, RegMem::reg(tmp_xmm.to_reg()));
             inst.emit(&[], sink, info, state);
 
             let handle_large = sink.get_label();
@@ -3710,8 +3688,8 @@ pub(crate) fn emit(
                 let inst = Inst::alu_rmi_r(
                     *dst_size,
                     AluRmiROpcode::Xor,
-                    RegMemImm::reg(dst),
-                    Writable::from_reg(dst),
+                    RegMemImm::reg(dst.to_reg()),
+                    dst,
                 );
                 inst.emit(&[], sink, info, state);
 
@@ -3727,10 +3705,10 @@ pub(crate) fn emit(
             // Actual truncation for small inputs: if the result is not positive, then we had an
             // overflow.
 
-            let inst = Inst::xmm_to_gpr(trunc_op, src, Writable::from_reg(dst), *dst_size);
+            let inst = Inst::xmm_to_gpr(trunc_op, src, dst, *dst_size);
             inst.emit(&[], sink, info, state);
 
-            let inst = Inst::cmp_rmi_r(*dst_size, dst, RegMemImm::imm(0));
+            let inst = Inst::cmp_rmi_r(*dst_size, dst.to_reg(), RegMemImm::imm(0));
             inst.emit(&[], sink, info, state);
 
             one_way_jmp(sink, CC::NL, done); // if dst >= 0, jump to done
@@ -3741,8 +3719,8 @@ pub(crate) fn emit(
                 let inst = Inst::alu_rmi_r(
                     *dst_size,
                     AluRmiROpcode::Xor,
-                    RegMemImm::reg(dst),
-                    Writable::from_reg(dst),
+                    RegMemImm::reg(dst.to_reg()),
+                    dst,
                 );
                 inst.emit(&[], sink, info, state);
 
@@ -3758,16 +3736,16 @@ pub(crate) fn emit(
 
             sink.bind_label(handle_large, state.ctrl_plane_mut());
 
-            let inst = Inst::gen_move(Writable::from_reg(tmp_xmm2), src, types::F64);
+            let inst = Inst::gen_move(tmp_xmm2, src, types::F64);
             inst.emit(&[], sink, info, state);
 
-            let inst = Inst::xmm_rm_r(sub_op, RegMem::reg(tmp_xmm), Writable::from_reg(tmp_xmm2));
+            let inst = Inst::xmm_rm_r(sub_op, RegMem::reg(tmp_xmm.to_reg()), tmp_xmm2);
             inst.emit(&[], sink, info, state);
 
-            let inst = Inst::xmm_to_gpr(trunc_op, tmp_xmm2, Writable::from_reg(dst), *dst_size);
+            let inst = Inst::xmm_to_gpr(trunc_op, tmp_xmm2.to_reg(), dst, *dst_size);
             inst.emit(&[], sink, info, state);
 
-            let inst = Inst::cmp_rmi_r(*dst_size, dst, RegMemImm::imm(0));
+            let inst = Inst::cmp_rmi_r(*dst_size, dst.to_reg(), RegMemImm::imm(0));
             inst.emit(&[], sink, info, state);
 
             if *is_saturating {
@@ -3783,7 +3761,7 @@ pub(crate) fn emit(
                     } else {
                         u32::max_value() as u64
                     },
-                    Writable::from_reg(dst),
+                    dst,
                 );
                 inst.emit(&[], sink, info, state);
 
@@ -3796,14 +3774,14 @@ pub(crate) fn emit(
             }
 
             if *dst_size == OperandSize::Size64 {
-                let inst = Inst::imm(OperandSize::Size64, 1 << 63, Writable::from_reg(tmp_gpr));
+                let inst = Inst::imm(OperandSize::Size64, 1 << 63, tmp_gpr);
                 inst.emit(&[], sink, info, state);
 
                 let inst = Inst::alu_rmi_r(
                     OperandSize::Size64,
                     AluRmiROpcode::Add,
-                    RegMemImm::reg(tmp_gpr),
-                    Writable::from_reg(dst),
+                    RegMemImm::reg(tmp_gpr.to_reg()),
+                    dst,
                 );
                 inst.emit(&[], sink, info, state);
             } else {
@@ -3811,7 +3789,7 @@ pub(crate) fn emit(
                     OperandSize::Size32,
                     AluRmiROpcode::Add,
                     RegMemImm::imm(1 << 31),
-                    Writable::from_reg(dst),
+                    dst,
                 );
                 inst.emit(&[], sink, info, state);
             }
diff --git a/crates/wasmtime/src/runtime/externals/global.rs b/crates/wasmtime/src/runtime/externals/global.rs
index 4623bce68..ae2b1e460 100644
--- a/crates/wasmtime/src/runtime/externals/global.rs
+++ b/crates/wasmtime/src/runtime/externals/global.rs
@@ -216,7 +216,7 @@ impl Global {
 
     pub(crate) fn trace_root(&self, store: &mut StoreOpaque, gc_roots_list: &mut GcRootsList) {
         if let Some(ref_ty) = self._ty(store).content().as_ref() {
-            if !ref_ty.is_gc_heap_type() {
+            if !ref_ty.is_vmgcref_type_and_points_to_object() {
                 return;
             }
 
diff --git a/crates/wasmtime/src/runtime/externals/table.rs b/crates/wasmtime/src/runtime/externals/table.rs
index db63ca8e6..67d1aec8c 100644
--- a/crates/wasmtime/src/runtime/externals/table.rs
+++ b/crates/wasmtime/src/runtime/externals/table.rs
@@ -375,7 +375,11 @@ impl Table {
     }
 
     pub(crate) fn trace_roots(&self, store: &mut StoreOpaque, gc_roots_list: &mut GcRootsList) {
-        if !self._ty(store).element().is_gc_heap_type() {
+        if !self
+            ._ty(store)
+            .element()
+            .is_vmgcref_type_and_points_to_object()
+        {
             return;
         }
 
diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs
index 3e0611766..3579a2049 100644
--- a/crates/wasmtime/src/runtime/func.rs
+++ b/crates/wasmtime/src/runtime/func.rs
@@ -90,12 +90,14 @@ impl NoFunc {
 
     /// Get the null `(ref null nofunc)` (aka `nullfuncref`) reference as a
     /// [`Ref`].
+    #[inline]
     pub fn null_ref() -> Ref {
         Ref::Func(None)
     }
 
     /// Get the null `(ref null nofunc)` (aka `nullfuncref`) reference as a
     /// [`Val`].
+    #[inline]
     pub fn null_val() -> Val {
         Val::FuncRef(None)
     }
@@ -620,25 +622,27 @@ impl Func {
     /// function being called is known statically so the type signature can
     /// be inferred. Rust types will map to WebAssembly types as follows:
     ///
-    /// | Rust Argument Type                | WebAssembly Type                      |
-    /// |-----------------------------------|---------------------------------------|
-    /// | `i32`                             | `i32`                                 |
-    /// | `u32`                             | `i32`                                 |
-    /// | `i64`                             | `i64`                                 |
-    /// | `u64`                             | `i64`                                 |
-    /// | `f32`                             | `f32`                                 |
-    /// | `f64`                             | `f64`                                 |
-    /// | `V128` on x86-64 and aarch64 only | `v128`                                |
-    /// | `Option<Func>`                    | `funcref` aka `(ref null func)`       |
-    /// | `Func`                            | `(ref func)`                          |
-    /// | `Option<Nofunc>`                  | `nullfuncref` aka `(ref null nofunc)` |
-    /// | `NoFunc`                          | `(ref nofunc)`                        |
-    /// | `Option<ExternRef>`               | `externref` aka `(ref null extern)`   |
-    /// | `ExternRef`                       | `(ref extern)`                        |
-    /// | `Option<AnyRef>`                  | `anyref` aka `(ref null any)`         |
-    /// | `AnyRef`                          | `(ref any)`                           |
-    /// | `Option<I31>`                     | `i31ref` aka `(ref null i31)`         |
-    /// | `I31`                             | `(ref i31)`                           |
+    /// | Rust Argument Type                | WebAssembly Type                          |
+    /// |-----------------------------------|-------------------------------------------|
+    /// | `i32`                             | `i32`                                     |
+    /// | `u32`                             | `i32`                                     |
+    /// | `i64`                             | `i64`                                     |
+    /// | `u64`                             | `i64`                                     |
+    /// | `f32`                             | `f32`                                     |
+    /// | `f64`                             | `f64`                                     |
+    /// | `V128` on x86-64 and aarch64 only | `v128`                                    |
+    /// | `Option<Func>`                    | `funcref` aka `(ref null func)`           |
+    /// | `Func`                            | `(ref func)`                              |
+    /// | `Option<Nofunc>`                  | `nullfuncref` aka `(ref null nofunc)`     |
+    /// | `NoFunc`                          | `(ref nofunc)`                            |
+    /// | `Option<ExternRef>`               | `externref` aka `(ref null extern)`       |
+    /// | `ExternRef`                       | `(ref extern)`                            |
+    /// | `Option<NoExtern>`                | `nullexternref` aka `(ref null noextern)` |
+    /// | `NoExtern`                        | `(ref noextern)`                          |
+    /// | `Option<AnyRef>`                  | `anyref` aka `(ref null any)`             |
+    /// | `AnyRef`                          | `(ref any)`                               |
+    /// | `Option<I31>`                     | `i31ref` aka `(ref null i31)`             |
+    /// | `I31`                             | `(ref i31)`                               |
     ///
     /// Any of the Rust types can be returned from the closure as well, in
     /// addition to some extra types
@@ -1391,25 +1395,27 @@ impl Func {
     ///
     /// Translation between Rust types and WebAssembly types looks like:
     ///
-    /// | WebAssembly                           | Rust                                  |
-    /// |---------------------------------------|---------------------------------------|
-    /// | `i32`                                 | `i32` or `u32`                        |
-    /// | `i64`                                 | `i64` or `u64`                        |
-    /// | `f32`                                 | `f32`                                 |
-    /// | `f64`                                 | `f64`                                 |
-    /// | `externref` aka `(ref null extern)`   | `Option<ExternRef>`                   |
-    /// | `(ref extern)`                        | `ExternRef`                           |
-    /// | `anyref` aka `(ref null any)`         | `Option<AnyRef>`                      |
-    /// | `(ref any)`                           | `AnyRef`                              |
-    /// | `i31ref` aka `(ref null i31)`         | `Option<I31>`                         |
-    /// | `(ref i31)`                           | `I31`                                 |
-    /// | `funcref` aka `(ref null func)`       | `Option<Func>`                        |
-    /// | `(ref func)`                          | `Func`                                |
-    /// | `(ref null <func type index>)`        | `Option<Func>`                        |
-    /// | `(ref <func type index>)`             | `Func`                                |
-    /// | `nullfuncref` aka `(ref null nofunc)` | `Option<NoFunc>`                      |
-    /// | `(ref nofunc)`                        | `NoFunc`                              |
-    /// | `v128`                                | `V128` on `x86-64` and `aarch64` only |
+    /// | WebAssembly                               | Rust                                  |
+    /// |-------------------------------------------|---------------------------------------|
+    /// | `i32`                                     | `i32` or `u32`                        |
+    /// | `i64`                                     | `i64` or `u64`                        |
+    /// | `f32`                                     | `f32`                                 |
+    /// | `f64`                                     | `f64`                                 |
+    /// | `externref` aka `(ref null extern)`       | `Option<ExternRef>`                   |
+    /// | `(ref extern)`                            | `ExternRef`                           |
+    /// | `(ref noextern)`                          | `NoExtern`                            |
+    /// | `nullexternref` aka `(ref null noextern)` | `Option<NoExtern>`                    |
+    /// | `anyref` aka `(ref null any)`             | `Option<AnyRef>`                      |
+    /// | `(ref any)`                               | `AnyRef`                              |
+    /// | `i31ref` aka `(ref null i31)`             | `Option<I31>`                         |
+    /// | `(ref i31)`                               | `I31`                                 |
+    /// | `funcref` aka `(ref null func)`           | `Option<Func>`                        |
+    /// | `(ref func)`                              | `Func`                                |
+    /// | `(ref null <func type index>)`            | `Option<Func>`                        |
+    /// | `(ref <func type index>)`                 | `Func`                                |
+    /// | `nullfuncref` aka `(ref null nofunc)`     | `Option<NoFunc>`                      |
+    /// | `(ref nofunc)`                            | `NoFunc`                              |
+    /// | `v128`                                    | `V128` on `x86-64` and `aarch64` only |
     ///
     /// (Note that this mapping is the same as that of [`Func::wrap`]).
     ///
diff --git a/crates/wasmtime/src/runtime/func/typed.rs b/crates/wasmtime/src/runtime/func/typed.rs
index 0fa6f01bd..79777dea4 100644
--- a/crates/wasmtime/src/runtime/func/typed.rs
+++ b/crates/wasmtime/src/runtime/func/typed.rs
@@ -154,7 +154,7 @@ where
         #[cfg(feature = "gc")]
         {
             // See the comment in `Func::call_impl_check_args`.
-            let num_gc_refs = _params.non_i31_gc_refs_count();
+            let num_gc_refs = _params.vmgcref_pointing_to_object_count();
             if let Some(num_gc_refs) = NonZeroUsize::new(num_gc_refs) {
                 return _store
                     .unwrap_gc_store()
@@ -329,9 +329,18 @@ pub unsafe trait WasmTy: Send {
         actual: &HeapType,
     ) -> Result<()>;
 
-    // Is this an externref?
+    // Is this a GC-managed reference that actually points to a GC object? That
+    // is, `self` is *not* an `i31`, null reference, or uninhabited type.
+    //
+    // Note that it is okay if this returns false positives (i.e. `true` for
+    // `Rooted<AnyRef>` without actually looking up the rooted `anyref` in the
+    // store and reflecting on it to determine whether it is actually an
+    // `i31`). However, it is not okay if this returns false negatives.
     #[doc(hidden)]
-    fn is_non_i31_gc_ref(&self) -> bool;
+    #[inline]
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
+        Self::valtype().is_vmgcref_type_and_points_to_object()
+    }
 
     // Construct a `Self::Abi` from the given `ValRaw`.
     #[doc(hidden)]
@@ -394,10 +403,6 @@ macro_rules! integers {
                 unreachable!()
             }
             #[inline]
-            fn is_non_i31_gc_ref(&self) -> bool {
-                false
-            }
-            #[inline]
             unsafe fn abi_from_raw(raw: *mut ValRaw) -> $primitive {
                 (*raw).$get_primitive()
             }
@@ -442,10 +447,6 @@ macro_rules! floats {
                 unreachable!()
             }
             #[inline]
-            fn is_non_i31_gc_ref(&self) -> bool {
-                false
-            }
-            #[inline]
             unsafe fn abi_from_raw(raw: *mut ValRaw) -> $float {
                 $float::from_bits((*raw).$get_float())
             }
@@ -490,7 +491,7 @@ unsafe impl WasmTy for NoFunc {
     }
 
     #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
         match self._inner {}
     }
 
@@ -543,11 +544,6 @@ unsafe impl WasmTy for Option<NoFunc> {
         }
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        false
-    }
-
     #[inline]
     unsafe fn abi_from_raw(_raw: *mut ValRaw) -> Self::Abi {
         ptr::null_mut()
@@ -594,11 +590,6 @@ unsafe impl WasmTy for Func {
             .context("argument type mismatch for reference to concrete type")
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        false
-    }
-
     #[inline]
     unsafe fn abi_from_raw(raw: *mut ValRaw) -> Self::Abi {
         let p = (*raw).get_funcref();
@@ -656,11 +647,6 @@ unsafe impl WasmTy for Option<Func> {
         }
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        false
-    }
-
     #[inline]
     unsafe fn abi_from_raw(raw: *mut ValRaw) -> Self::Abi {
         (*raw).get_funcref() as Self::Abi
@@ -703,7 +689,7 @@ pub unsafe trait WasmParams: Send {
     ) -> Result<()>;
 
     #[doc(hidden)]
-    fn non_i31_gc_refs_count(&self) -> usize;
+    fn vmgcref_pointing_to_object_count(&self) -> usize;
 
     #[doc(hidden)]
     fn into_abi(self, store: &mut AutoAssertNoGc<'_>, func_ty: &FuncType) -> Result<Self::Abi>;
@@ -734,8 +720,8 @@ where
     }
 
     #[inline]
-    fn non_i31_gc_refs_count(&self) -> usize {
-        T::is_non_i31_gc_ref(self) as usize
+    fn vmgcref_pointing_to_object_count(&self) -> usize {
+        T::is_vmgcref_and_points_to_object(self) as usize
     }
 
     #[inline]
@@ -786,10 +772,10 @@ macro_rules! impl_wasm_params {
             }
 
             #[inline]
-            fn non_i31_gc_refs_count(&self) -> usize {
+            fn vmgcref_pointing_to_object_count(&self) -> usize {
                 let ($(ref $t,)*) = self;
                 0 $(
-                    + $t.is_non_i31_gc_ref() as usize
+                    + $t.is_vmgcref_and_points_to_object() as usize
                 )*
             }
 
diff --git a/crates/wasmtime/src/runtime/gc.rs b/crates/wasmtime/src/runtime/gc.rs
index 61ed99d8a..f6697b105 100644
--- a/crates/wasmtime/src/runtime/gc.rs
+++ b/crates/wasmtime/src/runtime/gc.rs
@@ -8,6 +8,9 @@ mod disabled;
 #[cfg(not(feature = "gc"))]
 pub use disabled::*;
 
+mod noextern;
+pub use noextern::NoExtern;
+
 use core::fmt;
 use core::ops::Deref;
 
diff --git a/crates/wasmtime/src/runtime/gc/enabled/anyref.rs b/crates/wasmtime/src/runtime/gc/enabled/anyref.rs
index 946293add..49344a97c 100644
--- a/crates/wasmtime/src/runtime/gc/enabled/anyref.rs
+++ b/crates/wasmtime/src/runtime/gc/enabled/anyref.rs
@@ -264,11 +264,6 @@ unsafe impl WasmTy for Rooted<AnyRef> {
         unreachable!()
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        true
-    }
-
     #[inline]
     unsafe fn abi_from_raw(raw: *mut ValRaw) -> Self::Abi {
         let raw = (*raw).get_externref();
@@ -320,8 +315,8 @@ unsafe impl WasmTy for Option<Rooted<AnyRef>> {
     }
 
     #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        true
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
+        self.is_some()
     }
 
     #[inline]
@@ -371,11 +366,6 @@ unsafe impl WasmTy for ManuallyRooted<AnyRef> {
         unreachable!()
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        true
-    }
-
     #[inline]
     unsafe fn abi_from_raw(raw: *mut ValRaw) -> Self::Abi {
         let externref = (*raw).get_externref();
@@ -432,8 +422,8 @@ unsafe impl WasmTy for Option<ManuallyRooted<AnyRef>> {
     }
 
     #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        true
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
+        self.is_some()
     }
 
     #[inline]
diff --git a/crates/wasmtime/src/runtime/gc/enabled/externref.rs b/crates/wasmtime/src/runtime/gc/enabled/externref.rs
index 7e389d4cb..a57382f9e 100644
--- a/crates/wasmtime/src/runtime/gc/enabled/externref.rs
+++ b/crates/wasmtime/src/runtime/gc/enabled/externref.rs
@@ -441,11 +441,6 @@ unsafe impl WasmTy for Rooted<ExternRef> {
         unreachable!()
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        true
-    }
-
     #[inline]
     unsafe fn abi_from_raw(raw: *mut ValRaw) -> Self::Abi {
         let raw = (*raw).get_externref();
@@ -497,8 +492,8 @@ unsafe impl WasmTy for Option<Rooted<ExternRef>> {
     }
 
     #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        true
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
+        self.is_some()
     }
 
     #[inline]
@@ -549,7 +544,7 @@ unsafe impl WasmTy for ManuallyRooted<ExternRef> {
     }
 
     #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
         true
     }
 
@@ -609,8 +604,8 @@ unsafe impl WasmTy for Option<ManuallyRooted<ExternRef>> {
     }
 
     #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        true
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
+        self.is_some()
     }
 
     #[inline]
diff --git a/crates/wasmtime/src/runtime/gc/enabled/i31.rs b/crates/wasmtime/src/runtime/gc/enabled/i31.rs
index 9d632a6f2..ddf7f1b39 100644
--- a/crates/wasmtime/src/runtime/gc/enabled/i31.rs
+++ b/crates/wasmtime/src/runtime/gc/enabled/i31.rs
@@ -243,11 +243,6 @@ unsafe impl WasmTy for I31 {
         unreachable!()
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        false
-    }
-
     #[inline]
     unsafe fn abi_from_raw(raw: *mut ValRaw) -> Self::Abi {
         let raw = (*raw).get_anyref();
@@ -306,11 +301,6 @@ unsafe impl WasmTy for Option<I31> {
         unreachable!()
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        false
-    }
-
     #[inline]
     unsafe fn abi_from_raw(raw: *mut ValRaw) -> Self::Abi {
         let raw = (*raw).get_anyref();
diff --git a/crates/wasmtime/src/runtime/gc/noextern.rs b/crates/wasmtime/src/runtime/gc/noextern.rs
new file mode 100644
index 000000000..d7d4a0b3d
--- /dev/null
+++ b/crates/wasmtime/src/runtime/gc/noextern.rs
@@ -0,0 +1,179 @@
+use crate::{
+    store::{AutoAssertNoGc, StoreOpaque},
+    HeapType, Ref, RefType, Result, Uninhabited, Val, ValRaw, ValType, WasmTy,
+};
+
+/// A reference to the abstract `noextern` heap value.
+///
+/// The are no instances of `(ref noextern)`: it is an uninhabited type.
+///
+/// There is precisely one instance of `(ref null noextern)`, aka `nullexternref`:
+/// the null reference.
+///
+/// This `NoExtern` Rust type's sole purpose is for use with
+/// [`Func::wrap`][crate::Func::wrap]- and
+/// [`Func::typed`][crate::Func::typed]-style APIs for statically typing a
+/// function as taking or returning a `(ref null noextern)` (aka
+/// `Option<NoExtern>`) which is always `None`.
+///
+/// # Example
+///
+/// ```
+/// # use wasmtime::*;
+/// # fn _foo() -> Result<()> {
+/// let mut config = Config::new();
+/// config.wasm_function_references(true);
+/// config.wasm_gc(true);
+/// let engine = Engine::new(&config)?;
+///
+/// let module = Module::new(
+///     &engine,
+///     r#"
+///         (module
+///             (func (export "f") (param (ref null noextern))
+///                 ;; If the reference is null, return.
+///                 local.get 0
+///                 ref.is_null noextern
+///                 br_if 0
+///
+///                 ;; If the reference was not null (which is impossible)
+///                 ;; then raise a trap.
+///                 unreachable
+///             )
+///         )
+///     "#,
+/// )?;
+///
+/// let mut store = Store::new(&engine, ());
+/// let instance = Instance::new(&mut store, &module, &[])?;
+/// let f = instance.get_func(&mut store, "f").unwrap();
+///
+/// // We can cast a `(ref null noextern)`-taking function into a typed function that
+/// // takes an `Option<NoExtern>` via the `Func::typed` method.
+/// let f = f.typed::<Option<NoExtern>, ()>(&store)?;
+///
+/// // We can call the typed function, passing the null `noextern` reference.
+/// let result = f.call(&mut store, NoExtern::null());
+///
+/// // The function should not have trapped, because the reference we gave it was
+/// // null (as it had to be, since `NoExtern` is uninhabited).
+/// assert!(result.is_ok());
+/// # Ok(())
+/// # }
+/// ```
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct NoExtern {
+    _inner: Uninhabited,
+}
+
+impl NoExtern {
+    /// Get the null `(ref null noextern)` (aka `nullexternref`) reference.
+    #[inline]
+    pub fn null() -> Option<Self> {
+        None
+    }
+
+    /// Get the null `(ref null noextern)` (aka `nullexternref`) reference as a
+    /// [`Ref`].
+    #[inline]
+    pub fn null_ref() -> Ref {
+        Ref::Extern(None)
+    }
+
+    /// Get the null `(ref null noextern)` (aka `nullexternref`) reference as a
+    /// [`Val`].
+    #[inline]
+    pub fn null_val() -> Val {
+        Val::ExternRef(None)
+    }
+}
+
+unsafe impl WasmTy for NoExtern {
+    type Abi = NoExtern;
+
+    #[inline]
+    fn valtype() -> ValType {
+        ValType::Ref(RefType::new(false, HeapType::NoExtern))
+    }
+
+    #[inline]
+    fn compatible_with_store(&self, _store: &StoreOpaque) -> bool {
+        match self._inner {}
+    }
+
+    #[inline]
+    fn dynamic_concrete_type_check(&self, _: &StoreOpaque, _: bool, _: &HeapType) -> Result<()> {
+        match self._inner {}
+    }
+
+    #[inline]
+    fn is_vmgcref_and_points_to_object(&self) -> bool {
+        match self._inner {}
+    }
+
+    #[inline]
+    unsafe fn abi_from_raw(_raw: *mut ValRaw) -> Self::Abi {
+        unreachable!("NoExtern is uninhabited")
+    }
+
+    #[inline]
+    unsafe fn abi_into_raw(abi: Self::Abi, _raw: *mut ValRaw) {
+        match abi._inner {}
+    }
+
+    #[inline]
+    fn into_abi(self, _store: &mut AutoAssertNoGc<'_>) -> Result<Self::Abi> {
+        unreachable!("NoExtern is uninhabited")
+    }
+
+    #[inline]
+    unsafe fn from_abi(abi: Self::Abi, _store: &mut AutoAssertNoGc<'_>) -> Self {
+        match abi._inner {}
+    }
+}
+
+unsafe impl WasmTy for Option<NoExtern> {
+    type Abi = u32;
+
+    #[inline]
+    fn valtype() -> ValType {
+        ValType::Ref(RefType::new(true, HeapType::NoExtern))
+    }
+
+    #[inline]
+    fn compatible_with_store(&self, _store: &StoreOpaque) -> bool {
+        true
+    }
+
+    #[inline]
+    fn dynamic_concrete_type_check(
+        &self,
+        _store: &StoreOpaque,
+        _nullable: bool,
+        _ty: &HeapType,
+    ) -> Result<()> {
+        unreachable!()
+    }
+
+    #[inline]
+    unsafe fn abi_from_raw(_raw: *mut ValRaw) -> Self::Abi {
+        0
+    }
+
+    #[inline]
+    unsafe fn abi_into_raw(abi: Self::Abi, raw: *mut ValRaw) {
+        debug_assert_eq!(abi, 0);
+        *raw = ValRaw::externref(0);
+    }
+
+    #[inline]
+    fn into_abi(self, _store: &mut AutoAssertNoGc<'_>) -> Result<Self::Abi> {
+        Ok(0)
+    }
+
+    #[inline]
+    unsafe fn from_abi(abi: Self::Abi, _store: &mut AutoAssertNoGc<'_>) -> Self {
+        debug_assert_eq!(abi, 0);
+        None
+    }
+}
diff --git a/crates/wasmtime/src/runtime/types.rs b/crates/wasmtime/src/runtime/types.rs
index a23782883..eb62df488 100644
--- a/crates/wasmtime/src/runtime/types.rs
+++ b/crates/wasmtime/src/runtime/types.rs
@@ -238,6 +238,16 @@ impl ValType {
         a.matches(b) && b.matches(a)
     }
 
+    /// Is this a `VMGcRef` type that is not i31 and is not an uninhabited
+    /// bottom type?
+    #[inline]
+    pub(crate) fn is_vmgcref_type_and_points_to_object(&self) -> bool {
+        match self {
+            ValType::Ref(r) => r.is_vmgcref_type_and_points_to_object(),
+            ValType::I32 | ValType::I64 | ValType::F32 | ValType::F64 | ValType::V128 => false,
+        }
+    }
+
     pub(crate) fn ensure_matches(&self, engine: &Engine, other: &ValType) -> Result<()> {
         if !self.comes_from_same_engine(engine) || !other.comes_from_same_engine(engine) {
             bail!("type used with wrong engine");
@@ -425,7 +435,7 @@ impl RefType {
         }
     }
 
-    pub(crate) fn is_gc_heap_type(&self) -> bool {
+    pub(crate) fn is_vmgcref_type_and_points_to_object(&self) -> bool {
         self.heap_type().is_vmgcref_type_and_points_to_object()
     }
 }
@@ -1032,7 +1042,11 @@ impl HeapType {
     /// bottom type?
     #[inline]
     pub(crate) fn is_vmgcref_type_and_points_to_object(&self) -> bool {
-        self.is_vmgcref_type() && !matches!(self, HeapType::I31 | HeapType::NoFunc | HeapType::None)
+        self.is_vmgcref_type()
+            && !matches!(
+                self,
+                HeapType::I31 | HeapType::NoExtern | HeapType::NoFunc | HeapType::None
+            )
     }
 }
 
diff --git a/crates/wasmtime/src/runtime/v128.rs b/crates/wasmtime/src/runtime/v128.rs
index 287958c30..2e6e5affa 100644
--- a/crates/wasmtime/src/runtime/v128.rs
+++ b/crates/wasmtime/src/runtime/v128.rs
@@ -104,11 +104,6 @@ unsafe impl WasmTy for V128 {
         unreachable!()
     }
 
-    #[inline]
-    fn is_non_i31_gc_ref(&self) -> bool {
-        false
-    }
-
     #[inline]
     unsafe fn abi_from_raw(raw: *mut ValRaw) -> Self::Abi {
         V128::from((*raw).get_v128()).0


Test patch:
diff --git a/tests/all/func.rs b/tests/all/func.rs
index eafff2bf3..ecf8feb52 100644
--- a/tests/all/func.rs
+++ b/tests/all/func.rs
@@ -400,6 +400,8 @@ fn func_constructors() {
     Func::wrap(&mut store, || -> Option<Func> { None });
     Func::wrap(&mut store, || -> NoFunc { loop {} });
     Func::wrap(&mut store, || -> Option<NoFunc> { None });
+    Func::wrap(&mut store, || -> NoExtern { loop {} });
+    Func::wrap(&mut store, || -> Option<NoExtern> { None });
 
     Func::wrap(&mut store, || -> Result<()> { loop {} });
     Func::wrap(&mut store, || -> Result<i32> { loop {} });
@@ -429,6 +431,8 @@ fn func_constructors() {
     Func::wrap(&mut store, || -> Result<Option<Func>> { loop {} });
     Func::wrap(&mut store, || -> Result<NoFunc> { loop {} });
     Func::wrap(&mut store, || -> Result<Option<NoFunc>> { loop {} });
+    Func::wrap(&mut store, || -> Result<NoExtern> { loop {} });
+    Func::wrap(&mut store, || -> Result<Option<NoExtern>> { loop {} });
 }
 
 #[test]
diff --git a/tests/all/main.rs b/tests/all/main.rs
index bdfed81d4..fcab06d35 100644
--- a/tests/all/main.rs
+++ b/tests/all/main.rs
@@ -28,6 +28,7 @@ mod memory_creator;
 mod module;
 mod module_serialize;
 mod name;
+mod noextern;
 mod piped_tests;
 mod pooling_allocator;
 mod relocs;
diff --git a/tests/all/noextern.rs b/tests/all/noextern.rs
new file mode 100644
index 000000000..59394d4f1
--- /dev/null
+++ b/tests/all/noextern.rs
@@ -0,0 +1,71 @@
+use std::sync::atomic::{AtomicU32, Ordering};
+use wasmtime::*;
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn func_wrap_no_extern() -> Result<()> {
+    let mut config = Config::default();
+    config.wasm_function_references(true);
+    config.wasm_gc(true);
+
+    let engine = Engine::new(&config)?;
+
+    let module = Module::new(
+        &engine,
+        r#"
+            (module
+                (import "" "" (func (param nullexternref) (result nullexternref)))
+                (func $main
+                    ref.null noextern
+                    call 0
+                    drop
+                )
+                (start $main)
+            )
+        "#,
+    )?;
+
+    let mut store = Store::new(&engine, ());
+
+    static HITS: AtomicU32 = AtomicU32::new(0);
+
+    let f = Func::wrap(&mut store, |x: Option<NoExtern>| -> Option<NoExtern> {
+        assert!(x.is_none());
+        HITS.fetch_add(1, Ordering::SeqCst);
+        x
+    });
+
+    let _ = Instance::new(&mut store, &module, &[f.into()])?;
+    assert_eq!(HITS.load(Ordering::SeqCst), 1);
+
+    Ok(())
+}
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn func_typed_no_extern() -> Result<()> {
+    let mut config = Config::default();
+    config.wasm_function_references(true);
+    config.wasm_gc(true);
+
+    let engine = Engine::new(&config)?;
+
+    let module = Module::new(
+        &engine,
+        r#"
+            (module
+                (func (export "f") (param nullexternref) (result nullexternref)
+                    local.get 0
+                )
+            )
+        "#,
+    )?;
+
+    let mut store = Store::new(&engine, ());
+    let instance = Instance::new(&mut store, &module, &[])?;
+    let f = instance.get_typed_func::<Option<NoExtern>, Option<NoExtern>>(&mut store, "f")?;
+    let result = f.call(&mut store, None)?;
+    assert!(result.is_none());
+
+    Ok(())
+}