An attempt at a toy Vm
Missing Error Checking

I usually use c++ so it may not be best practice for c.

In C++ when memory allocation fails in new an exception is thrown, this is not the case in the C programming language when using malloc(), calloc() or realloc(). An additional check is required after any memory allocation call. The check is to see if the memory returned is NULL or not, if the allocation fails references through the pointer are Unknown Behavior.
Vm *new_vm() {
    Vm *ret = (Vm*)malloc(sizeof(Vm));
    if (!ret)
    {
        fprintf(stderr, &quot;Allocation of the Virtual Machine failed.\n&quot;);
        return ret;
    }
    
    ret-&gt;pc = 1024; /* add space for the stack */
    ret-&gt;sp = -1;
    ret-&gt;running = true;
    return ret;
}

In main():
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;vm.h&quot;

int main() {
    Vm *vm = new_vm();
    if (vm == NULL)
    {
        fprintf(stderr, &quot;Exiting Toy Virtual Machine.\n&quot;);
        return EXIT_FAILURE;
    }
    i32 buffer[] = {
            0x00000A01, /* push 0x0A(\n) */
            0x00004301, /* push 0x43(C)  */
            0x00004201, /* push 0x42(B)  */
            0x00004101, /* push 0x41(A)  */
            0x00000009, /* output */
            0x00000002, /* pop */
            0x00000009,
            0x00000002,
            0x00000009,
            0x00000002,
            0x00000009,
            0x00000000  /* halt */
    };
    for (int i = 0; i &lt; sizeof(buffer); i++) {
        vm-&gt;mem[vm-&gt;pc+i] = buffer[i];
    }
    run_vm(vm);
    free_vm(vm);
    return EXIT_SUCCESS;
}

Include Only Necessary Headers
In the code as posted, stdlib.h is included in vm.h, stdlib.h is not necessary in main(), only in vm.c. To reduce the scope of the header files and source files only include what is needed. Amoung other things, this will reduce compile times, and may also reduce linking problems.
Missing Default Case in Switch Statement
It is generally a good programming practice to have a default : case statement in a switch statement to handle cases that haven't been specified yet:
static void execute(Vm *vm) {
    switch(vm-&gt;opc) {
        default:
            fprintf(stderr, &quot;Unknown Opcode in execute(). 0x%x\n&quot;, vm-&gt;opc);
            return;
        case 0x00: /* halt */
            vm-&gt;running = false;
            printf(&quot;Halt\n&quot;);
            break;
        ...
    }

Then all possible paths through the function have been implemented. This is true in either C or C++ and most other programming languages that have a switch statement.
Use an ENUM for the Opcodes
The code would be much more readable if less numeric constants and more symbolic constants were used. In C there are 2 ways to do this, to create single symbolic constants use macro definition
#define SYMBOL  VALUE

or to use enums
typedef enum {
    HALT = 0x00,
    PUSH = 0x01,
    POP = 0x02,
    ...
    INPUTCHAR = 0x0A
} OPCODE;


typedef struct {
    i32 mem[0xffff]; /* approx. 64k */
    si32 sp;
    i32 pc;

    i32 raw;
    OPCODE opc;
    i32 param;

    bool running;
} Vm;

Use Unsigned Types as Indexes
The stack pointer index is currently a signed integer and is initialized to -1, this is what I consider to be a bad practice since stack[-1] will cause Unknown Behavior. It would be better to use size_t or unsigned as the stack pointer index. This will force a change in a number of areas, but here is what I would recommend:

Initialize running to false rather than true.
Initialize sp to zero
Only index the stack and increment the stack pointer if running is true
At the beginning of run_vm(Vm *vm) before the loop set running to true
Change the implementation of reset_vm(Vm *vm) to match all of the above

Type Names and Variable Names
Initially I was confused about Vm, whether it was a Virtual Memory or a Virtual Machine, this was true of other variables and types as well. Well written code is self documenting and doesn't need a lot of comments, type names and variable names play a big part in this. In my opinion Vm should be renamed VirtualMachine, sp should be renamed StackPointer, pc should be renamed ProgramCounter etc.
Use Library Functions Where Available
C++ contains std::memset(), and memset() in C predates C++. The function reset_vm() should use memset() rather than the loop it is using to reset the memory. The function memset() should be faster than the current loop.
void reset_vm(Vm *vm) {
    vm-&gt;running = true;
    memset(&amp;vm-&gt;mem[0], 0, sizeof(*vm-&gt;mem[0]) * 0xffff);
    vm-&gt;sp = -1;
    vm-&gt;pc = 1024;
}