For beginners ability change IRQ handlers i suggest add attach/detach functionality to all IRQ handlers (as i understand weak not enough for this). So easy change handlers to custom. For shared IRQ handlers (for example USB/CAN) suggest attach function return false for originial processing or true for exit from original IRQ handler. Or tthis rules implement for all attach functions.
For example
Serial1.attach(custom_function)
struct usart_dev usart_dev_table[] = {
    [USART1] = {
        .base = (usart_port*)USART1_BASE,
        .rcc_dev_num = RCC_USART1,
        .nvic_dev_num = NVIC_USART1,
	.handler = NULL
    },
...
// Or uint8(*handler)(void)
void usart_attach(uint8 usart_num, void (*handler)(void))
{
	usart_dev_table[usart_num].handler = handler;
}
void usart_detach(uint8 usart_num)
{
	usart_dev_table[usart_num].handler = NULL;
}
static inline void usart_irq(int usart_num) {
	if (usart_dev_table[usart_num].handler != NULL)
	{
		(usart_dev_table[usart_num].handler)();
		if ((usart_dev_table[usart_num].handler)())
			return;
	}
#ifdef USART_SAFE_INSERT
	    /* Ignore old bytes if the user defines USART_SAFE_INSERT. */
		rb_safe_insert(&(usart_dev_table[usart_num].rb), (uint8)((usart_dev_table[usart_num].base)->DR));
#else
    /* By default, push bytes around in the ring buffer. */
		rb_push_insert(&(usart_dev_table[usart_num].rb), (uint8)((usart_dev_table[usart_num].base)->DR));
#endif
}
void USART1_IRQHandler(void) {
    usart_irq(USART1);
}
hope in some situation it's helpfull