Code working in a sketch, but not in a library.

Hello, i've been trying to program a scheduler (Protothread) library and i've stubmled upon a weird problem.
The thing is, when I declare the functions that are in my lib in a simple sketch with no classes the code works just fine. But if I use the library it starts doing really weird stuff.

So let me explain step by step.

First I wrote my library that contains this code:

EasyThread.h

#ifndef EasyThread_h
    #define EasyThread_h
    #include "Arduino.h"
#endif

class EZThread {
    public:
        // Constructor takes the number of threads
        EZThread(int threads);

        // Protoype for the new Thread function
        void newThread(void (*f)(void), int time);

        // Prototype for the schedule function
        void schedule();
    private:
        // Unsigned long pointer that will point to an array containing the timers for every thread
        unsigned long *_schedules;
        // int pointer that will point to an array containing the intervals for every thread
        int *_intervals;
        // Function double pointer that will point to an array of function pointers that point to
        // the function callback of every thread
        void (**_f)();
        // Int that stores the number of threads there are
        int _schid;
};

EasyThread.cpp

#include "Arduino.h"
#include "EasyThread.h"

// Constructor declaration
EZThread::EZThread(int threads){
    // We create the unsigned long array that will store timers for the threads
    unsigned long schedules[threads];

    // We create the int array that will store the intervals at which every thread should execute
    int intervals[threads];

    // We create the function pointer array that will point to the callback for every thread.
    void (*f[threads])();

    //We assign the globals defined in EasyThread.h
    _f = f;
    _schedules = schedules;
    _intervals = intervals;
    _schid = 0;
}

// newThread declaration
// This function creates a new Thread
void EZThread::newThread(void (*f)(), int time){
    // We store the callback function adress to the function pointer array
    *(_f + _schid) = f;
    
    // We store the time interval at which the thread should execute
    *(_intervals + _schid) = time;

    // We initialize the timer for this thread to 0
    *(_schedules + _schid) = (unsigned long) 0;

    // We increase the number of threads
    _schid++;
}

// schedule declaration
// This function executes the threads that should be executed and resets it's timers
void EZThread::schedule(){
    // We store the current time in which we are scheduling
    unsigned long now = millis();

    // For every thread
    for(int i = 0; i < _schid; i++){
        // If the scheduling time - the last time  that thread executed >= interval at it should execute
        if(now - *(_schedules + i) >= *(_intervals + i)){
            // We call to the callback function stored in the function pointer array
            (*(_f + i))();

            // We set the internal timer to the time we started scheduling
            *(_schedules + i) = now;
        }
    }
}

Then, if I try to use my library with a simple sketch like this:

#include <EasyThread.h>

//Constructor with one thread
EZThread scheduler(1);

void setup() {
  //Start serial at 9600
  Serial.begin(9600);

  //Create new thread
  scheduler.newThread(printNice, 1000);
}

// Thread callback
void printNice(){
  Serial.println("Nice!");
}

void loop() {
  //Call to the scheduler
  scheduler.schedule();
}

I get this serial output:

12:28:14.681 ->

It should print "Nice" every second.

Now I thought, well there must be some error and the printNice callback is not executing so I put a Serial.println("Setup start"); just after the Serial.begin(9600); like this:

...
//New setup
void setup() {
  //Start serial at 9600
  Serial.begin(9600);
  //New print
  Serial.println("Setup start");
  //Create new thread
  scheduler.newThread(printNice, 1000);
}
...

I get this output:

Random gibberish and invalid characters. So somehow, and I don't know how, my library is messing with the serial output.
I figured out that if I remove the scheduler.schedule(); in the void loop(){} then the Serial.println("Setup start"); works just fine and outputs:

12:46:52.024 -> Setup start

I tried adding Serial.flush(); and messing around with the code only to get even weirder stuff on the serial like the timestamp disappearing and only printing the two first chars of the Serial.println("Setup start"); or even printing in the same line and timestamp over and over.
I ended up with a code that seemed to be resseting itself as the millis() value was going back to zero and the setup was being executed again. I also tried seeing if I was somehow using too much memory and that could cause a stack overflow and a reset of the program. But using GitHub - McNeight/MemoryFree: Arduino MemoryFree library. Hosting it on github for easy access. to check the remaining available memory prints:

13:38:11.961 -> Start
13:38:11.961 -> freeMemory()=1820
13:38:11.961 -> 7
13:38:11.961 -> freeMemory()=1820
13:38:12.992 -> 1030
13:38:12.992 -> freeMemory()=1820
13:38:13.990 -> 2036

So I don't think i'm running out of memory.

Then I finally tried to move the definitions and everything from the library to the sketch, and then it worked:

Sketch:

#include <MemoryFree.h>
#define LED_PIN 12

const int threads = 1;

unsigned long *_schedules;
int *_intervals;
void (**_f)();
int _schid;

void setup() {

  unsigned long schedules[threads];
  int intervals[threads];
  void (*f[threads])();
  _f = f;
  _schedules = schedules;
  _intervals = intervals;
  _schid = 0;
  
  Serial.begin(9600);
  Serial.println("Start");
  
  newThread(printNice, 1000);
  
  Serial.print("freeMemory()=");
  Serial.println(freeMemory());
}

void printNice(){
  Serial.println("Nice!");
}
void loop() {
  schedule();
  Serial.print("freeMemory()=");
  Serial.println(freeMemory());
}

void newThread(void (*f)(), int time){
    *(_f + _schid) = f;
    *(_intervals + _schid) = time;
    *(_schedules + _schid) = (unsigned long) 0;
    _schid++;
}

void schedule(){
    unsigned long now = millis();
    Serial.println(now);
    Serial.flush();
    for(int i = 0; i < _schid; i++){
        if(now - *(_schedules + i) >= *(_intervals + i)){
            (*(_f + i))();
            *(_schedules + i) = now;
        }
    }
}

Output:

Start
14:01:54.313 -> freeMemory()=1804
14:01:54.313 -> 0
14:01:54.313 -> freeMemory()=1804
....................
14:01:55.302 -> 975
14:01:55.336 -> freeMemory()=1804
14:01:55.336 -> 1001
14:01:55.336 -> Nice!

It's just the same code, but not in the library.
Im really confused and don't know what i'm missing. I would really appreciate some help or guidance on how to solve this problem.
Thanks in advance.

Some system information:
I'm running this in an Arduino UNO, but tested also in an Arduino Nano and got the same results.
Arduino IDE 1.8.13
Windows x64 and I'm using a USB 2.0 (I think)

// Constructor declaration

EZThread::EZThread(int threads){
    // We create the unsigned long array that will store timers for the threads
    unsigned long schedules[threads];

// We create the int array that will store the intervals at which every thread should execute
    int intervals[threads];

// We create the function pointer array that will point to the callback for every thread.
    void (*f[threads])();

//We assign the globals defined in EasyThread.h
    _f = f;
    _schedules = schedules;
    _intervals = intervals;
    _schid = 0;
}

This can never work. schedules, intervals and f are all local variables in the constructor. As soon as the constructor is complete, they go out of scope, but you still have dangling pointers to them in your object. Dereferencing these pointers is undefined behavior and produces gibberish or corrupts your stack.

You'll either have to use dynamically allocated arrays (use std::vector if you can), or make EZThread a template class, with the maximum number of threads as a template parameter, so you can avoid dynamic memory.

Small nitpick, the comment // Constructor declaration is incorrect, it's the constructor definition, not its declaration.
The declaration is in the header file: EZThread(int threads);

Pieter

This is not related to your problem, but the #endif should be at the end of the library:

#ifndef EasyThread_h
    #define EasyThread_h
    #include "Arduino.h"
#endif

This prevents the library from being loaded more than once, for example if another library that needs this library it will have an #include EasyThread.h. If you move the #endif to the end of the library, it can only be loaded once.