Interacting with the wrapped classes#

The Geant4 C++ types are wrapped using the package CxxWrap.jl. The Geant4 toolkit provides thousands of classes, of which only a sub-set are needed by developers to build applications. These are the ones that have wrapped. If you find missing classes, they can be added in subsequent releases of the Geant4.jl package.

This tutorial shows the basics of interacting with the wrapped classes and an aperçu of built-in functionally provided by CxxWrap.jl to make the interoperation of Julia and C++ as smooth as possible.

The first thing we need to do is to import the Geant4 module. All the wrapped Geant4 classes are implicitly exported since they are prefixed by G4, and therefore the chances of a name clash with other Julia symbols is minimized.

Object instantiation#

We start by instantiating some very simple object of type G4Box.

using Geant4
box = G4Box("MyBox", 1, 2, 3)  # the C++ contructor called G4Box (const G4String &pName, G4double pX, G4double pY, G4double pZ)
Geant4.G4BoxAllocated(Ptr{Nothing} @0x0000000002cdd2a0)

Note that:

  • the C++ constructor that is called is G4Box::G4Box(const G4String &pName, G4double pX, G4double pY, G4double pZ)

  • the conversion of Julia type Int64 to C++ G4double is implicit, as well as the String to G4String

The returned object is a pointer (Ptr) to the wrapped C++ object. You can see the printed memory address where the object resides. The CxxWrap package encodes (with the type Geant4.G4BoxAllocated) that the object has been allocated from Julia and will be by default garbage collected when not needed (i.e. no reference to it).

using Test                                  # the standard julia Test module is convenient here 
@test typeof(box) == Geant4.G4BoxAllocated  # the type is indeed a G4BoxAllocated
@test box isa G4Box                         # but is also a G4Box (G4BoxAllocated inherits from G4Box)

subtypes(G4Box)                             # this shows all the subtypes of G4Box
2-element Vector{Any}:
 Geant4.G4BoxAllocated
 Geant4.G4BoxDereferenced

The type hierarchy is as follows:

                +-------+
                |  Any  |
                +-------+
                    |
               +----------+
               | G4VSolid |
               +----------+
                    |
              +------------+
              | G4CSCSolid |
              +------------+
                    |
              +----------+
              |  G4Box   |
              +----------+
          |---------|-----------|
+------------------+ +-------------------+
|  G4BoxAllocated  | | G4BoxDereferenced |
+------------------+ +-------------------+

To take the C++ pointer or the reference of a wrapped object, the CxxWrap module provides the functions CxxPtr() and CxxRef(). This is often needed to comply with the Geant4 C++ interfaces. It is easy to add additional methods to avoid the burden of having to pass arguments by pointer or reference. This will be done to improve usability of popular classes. The Julia type CxxPtr{G4Box} represents a C++ pointer to G4Box (G4Box* in C++). The same for CxxRef{G4Box} as a reference.

r_box =  CxxRef(box)
p_box =  CxxPtr(box)
@test r_box isa CxxRef{G4Box}  # reference
@test p_box isa CxxPtr{G4Box}  # pointer
Test Passed

The user can dereference a CxxRef or CxxPtr with the operator []

@show typeof(r_box[])
@show typeof(p_box[])
@test r_box[] == box
@test p_box[] == box
typeof(r_box[]) = Geant4.G4BoxDereferenced
typeof(p_box[]) = Geant4.G4BoxDereferenced
Test Passed

Calling object methods#

In Julia, methods are instances of functions of a given name, of which the multi-dispatch functionality will select the best one that matches the actual argument types. To call a C++ method of a class we need to pass the wrapped object as first argument.

vol = GetCubicVolume(box)   # In C++ this would be box.GetCubicVolume()
@test vol == 8 * GetXHalfLength(box) * GetYHalfLength(box) * GetZHalfLength(box) 
Test Passed

We can see the methods defined for the function GetCubicVolume using the Julia builtin function methods()

methods(GetCubicVolume)
# 58 methods for generic function GetCubicVolume from Geant4:

Calling static class methods#

In this case we do not have an object instance. The way it is done is by concatenating the class name with the method names with the symbol !. This is an example:

G4Random!getTheSeed()   # This will call the C++ class method G4Random::getTheSeed()
1

Working with inheritance#

In Geant4 all solid types inherit from a common base class G4VSolid. The method Clone returns a pointer to this base class.

solid = Clone(box)
@test typeof(solid) == CxxPtr{G4VSolid}
@test CxxPtr(box) == box  # same instance
@test solid != box        # these are two diffrent instances
@test GetCubicVolume(solid) == GetCubicVolume(box)
Test Passed

Object ownership#

By default all objects instantiated by Julia will be garbage collected (deleted) by Julia. This may pose problems with Geant4 since in many occasions the ownership of the object is transferred implicitly to the toolkit, which will take care of doing a cleanup at the adequate moment. If we do not pay attention we may get crashes (use a deleted object or eventually double deletions).

There are nevertheless some exceptions that have been added to simplify the interactions for the following classes: G4PVPlacement, G4LogicalVolume, G4PVReplica, G4Material, G4Isotope, G4Element, G4JLParticleGun, G4JLDetectorConstruction, G4JLGeneratorAction, G4JLRunAction, G4JLSensDet, G4JLWorkerInitialization, G4JLStateDependent, G4LogicalSkinSurface, G4LogicalBorderSurface, G4OpticalSurface

The following code show the behavior when things are not done correctly

box1 = G4Box("box1", 1,1,3)                                   # construct boxes
box2 = G4Box("box2", 1,3,1)                                  
union = G4UnionSolid("union", CxxPtr(box1), CxxPtr(box2))     # construct a union solid with the two boxes
@show GetCubicVolume(union)
@show DistanceToIn(union, G4ThreeVector(10,10,10))
GetCubicVolume(union) = 40.00017222413403
DistanceToIn(union, G4ThreeVector(10, 10, 10)) = 
9.0
9.0

now force a GC

box1 = box2 = nothing                           # removed the reference to the G4Box objects
GC.gc()                                         # force a garbage collection now
@show GetCubicVolume(union)                     # G4BooleanSolid cashes the volume
# DistanceToIn(union, G4ThreeVector(10,10,10))  # this will probably crash the program
GetCubicVolume(union) = 40.00017222413403

40.00017222413403

To avoid this problem the user must transfer object ownership to the C++ side using the provided function move!(). For example:

box1 = G4Box("box1", 1,1,3)                                  # construct boxes
box2 = G4Box("box2", 1,3,1)                                  
union = G4UnionSolid("union", move!(box1), move!(box2))      # construct a union solid with the same box
@show GetCubicVolume(union)
@show DistanceToIn(union, G4ThreeVector(10,10,10));
GetCubicVolume(union) = 39.99985174389303
DistanceToIn(union, G4ThreeVector(10, 10, 10)) = 9.0

forcing now a GC will still work

box1 = box2 = zeros(1000)
GC.gc()
@show GetCubicVolume(union)                          # G4BooleanSolid cashes the volume
@show DistanceToIn(union, G4ThreeVector(10,10,10));  # now this will not crash the program
GetCubicVolume(union) = 39.99985174389303
DistanceToIn(union, G4ThreeVector(10, 10, 10)) = 9.0