RNTuple: MakeField causes termination of program

ROOT Version: 6.40.02

I’m trying to use a RNTupleParallelWriter to write a custom class. When I try doing this in the full framework we have (way too big to post here), it fails at the step of calling MakeField<myClass>("Hit"). I tried reproducing this using the ntpl014_framework.C tutorial, but using that it works fine.

What does work within our framework is to manually add a field for each member of the custom class and write those fields. So this pseudo-code works:

WriterClass::Constructor()

{
   ROOT::RNTupleWriteOptions options;

   options.SetUseBufferedWrite(true);

   auto model = ROOT::RNTupleModel::CreateBare();

   model->MakeField<UInt_t>("Address");
   model->MakeField<Float_t>("Charge");
   model->MakeField<Short_t>("KValue");
   model->MakeField<Float_t>("Cfd");
   model->MakeField<Long64_t>("TimeStamp");

   fTokens.push_back(model->GetToken("Address"));
   fTokens.push_back(model->GetToken("Charge"));
   fTokens.push_back(model->GetToken("KValue"));
   fTokens.push_back(model->GetToken("Cfd"));
   fTokens.push_back(model->GetToken("TimeStamp"));

   {
      std::lock_guard guard(fFileMutex);
      fEventWriter = ROOT::RNTupleParallelWriter::Append(std::move(model), "Hits", *fOutputFile, options);
   }

   fSlots.resize(fNumberOfSlots);
   for(auto& slot : fSlots) {
      slot.fillContext = fEventWriter->CreateFillContext();
      slot.entry = slot.fillContext->GetModel().CreateRawPtrWriteEntry();
   }
}

void WriterClass::Iteration(data from other part of the framework)
{
   UInt_t address = data->GetAddress();
   Float_t charge = data->GetCharge();
   Short_t kValue = data->GetKValue();
   Float_t cfd = data->GetCfd();
   Long64_t timeStamp = data->GetTimeStamp();
   fSlots[fCurrentSlot].entry->BindRawPtr(fTokens[0], &address);
   fSlots[fCurrentSlot].entry->BindRawPtr(fTokens[1], &charge);
   fSlots[fCurrentSlot].entry->BindRawPtr(fTokens[2], &kValue);
   fSlots[fCurrentSlot].entry->BindRawPtr(fTokens[3], &cfd);
   fSlots[fCurrentSlot].entry->BindRawPtr(fTokens[4], &timeStamp);

   code to call FillNoFlush on fillcontext and only FlushColumns and FlushCluster if asked to
}

But if I add a new class

class TSimpleHit {
public:
   explicit TSimpleHit(data* hit) : 
      fAddress(hit->GetAddress()), fCharge(hit->GetCharge()),
      fKValue(hit->GetKValue()), fCfd(hit->GetCfd()),
      fTimeStamp(hit->GetTimeStamp()) {}

private:
   UInt_t fAddress{0};
   Float_t fCharge{0.};
   Short_t fKValue{0};
   Float_t fCfd{0};
   Long64_t fTimeStamp{0};
};

and replace the five MakeField calls with a single one model->MakeField<TSimpleHit>("Hit"); the program fails at this step with

terminate called without an active exception
Aborted (core dumped)

I understand that it is hard to say what causes this since it works when using the tutorial, but maybe someone has encountered this before or has an idea what could cause this?

Thank you for the report!

Terminating without an error message is an issue. I will look into it.

The cause may be that the TSimpleHit has neither a default nor an I/O constructor. Having either is a requirement. I’ll check if I can reproduce it.

Thank you for checking this. I tried adding a default constructor to it (as I had in the simple tutorial code where it worked), but that didn’t change anything.

One more piece of information for this: it takes a long time for the program to terminate. I’ve put a cout message right before the line that crashes it, and it takes over 40 minutes for it to crash. EDIT: subsequent runs took only 5 to 10 minutes to crash, still a weirdly long time. During that time (between cout and crash) there is almost no CPU usage at all (maximum I saw using top was 0.3 %), and the state is S (sleeping).

I can also post the code I have based on the tutorial where it works if that helps?

Since you suggested that this might be due to the missing I/O support of the simple class I was using, I decided to try and compile the root-script I had (which was working with the same simple class) as a standalone program. Running that program gives me this error:

terminate called after throwing an instance of 'ROOT::RException'
  what():  RField: no I/O support for type TSimpleHit
At:
  TClass* {anonymous}::EnsureValidClass(std::string_view) [/github/home/ROOT-CI/src/tree/ntuple/src/RFieldMeta.cxx:62]

Aborted (core dumped)

So now I will try and see if I can add I/O support for this simple class to see if that works.

I added I/O support to the simple class, and that fixed things. I can now use the simple class in both the tutorial based program and in our framework. It still fails when I use a more complex class then the simple one posted here, but at least now I have something to test it with and compare it to.

The tip of looking at the I/O support was very helpful, thank you!

I tried using the more complex class with the tutorial based program and now I’m getting this error:

terminate called after throwing an instance of 'ROOT::RException'
  what():  TDetectorHit cannot be stored natively in RNTuple
At:
  void {anonymous}::EnsureValidUserClass(TClass*, const ROOT::RFieldBase&, std::string_view) [/github/home/ROOT-CI/src/tree/ntuple/src/RFieldMeta.cxx:89]

Aborted (core dumped)

I will look into this and see if I can find the reason why this class cannot be stored in NTuple. Could this be because it has a custom streamer?

EDIT: Looks like it. By replacing the auto-generated streamer of the simple class with

  void TSimpleHit::Streamer(TBuffer& R__b)
  {
     /// Stream an object of class TDetectorHit.
     if(R__b.IsReading()) {
        R__b.ReadClassBuffer(TSimpleHit::Class(), this);
     } else {
        fAddress = 10000;
        R__b.WriteClassBuffer(TSimpleHit::Class(), this);
     }
  }

I get the same error (cannot be stored natively) when using this simple class.

Could this be because it has a custom streamer?

Yes. For those cases, you need to use the work-around of using a StreamerField.

I get the same error (cannot be stored natively) when using this simple class.

This is still a custom streamer (even if it turns around and calls the default streaming).

However, you can easily transform this into a completely default streamer and an I/O customization rule:

#pragma read sourceClass="TSimpleHit" targetClass="TSimpleHit"  version="[1-]" \
   source = "" target = "fAddress" \
   code = "{ fAddress = 10000; }"

@pcanal thank you for that example code, I will try that out.

As far as I can see (from another question here on the forum), this goes into the LinkDef.h file. I also found the Input/Output chapter in the root user guide with some explanation in 1.5.6.1 about all the pragma variables.

I’ve removed (for now) the custom streamer from the more complex class and now I can write it into an RNTuple. I will try and update the LinkDef file with the new pragma and see if that solves all problems.