I have problems when using finalizer
s with my Julia v0.6
. I am using Atom when writing/running the code if this makes any difference.
As for the minimal working example, consider having the following C API:
extern "C" {
double d_somefunc(const double, void **);
double f_somefunc(const float, void **);
void delhandle(void *);
}
Above, I use void **
to change the pointer value, i.e., the second arguments to x_somefunc
are in/out arguments. Moreover, in this small example, void *
is simply an opaque pointer in the C++
library that is exposed to users for the C API.
After compiling the library as a shared object, I run the following Julia code to test the functioning of the library:
libhandle = Libdl.dlopen(joinpath(pwd(), "libexternal.so"))
funhandle = Libdl.dlsym(libhandle, :d_somefunc)
delhandle = Libdl.dlsym(libhandle, :delhandle)
mutable struct MyType{T<:Real}
val::T
handle::Ref{Ptr{Void}}
end
handle(m::MyType) = m.handle[]
value(m::MyType) = m.val
value!(m::MyType, val::Real) = (m.val = val)
function (::Type{MyType})(val::Real)
obj = MyType{typeof(val)}(val, Ref{Ptr{Void}}(0))
finalizer(obj, destruct!)
return obj
end
function destruct!(m::MyType)
println("Destructor called")
ccall(delhandle, Void, (Ptr{Void},), m.handle[])
end
function (m::MyType)()
return ccall(funhandle, Cdouble, (Cdouble, Ref{Ptr{Void}}), m.val, m.handle)
end
# Run below for 4 times
# for k in 1:4
m = MyType(1)
m()
m()
value!(m, 7)
# end
gc()
Behind the scenes, x_somefunc
functions check
- if
handle
(the second argument) points to an already allocated (by the C++ library) object of some typeMyClass
, - if not, they create a new object on the heap and change
handle
’s value to point to the corresponding object’s address, and, - they call the object’s
operator()
on theval
.
In other words, this is simply a stateful function call in the C++ library, which is hidden behind the opaque pointer void *
.
When I run the above commands until I get the garbage collector in Julia to do its job, I receive the following output (upon stopping Julia):
Destructor called
Destructor called
Destructor called
error in running finalizer: ErrorException("task switch not allowed from inside gc finalizer")
error in running finalizer: ErrorException("task switch not allowed from inside gc finalizer")
error in running finalizer: ErrorException("task switch not allowed from inside gc finalizer")
error in running finalizer: Base.ArgumentError(msg="stream is closed or unusable")
myptrs.size(): 1
myptrs.size(): 1
myptrs.size(): 2
myptrs.size(): 2
myptrs.size(): 3
myptrs.size(): 3
myptrs.size(): 4
myptrs.size(): 4
dtor called.
dtor called.
dtor called.
dtor called.
Apparently, the C++ library is successfully cleaning its mess upon exit (dlclose
). But I would like to know why I am getting the errors above in Julia.
Thank you for your time and help
EDIT. For those who would like to have a complete set of files to reproduce the error, below are the header and implementation files:
/* external.hpp file */
#ifndef EXTERNAL_HPP_
#define EXTERNAL_HPP_
template <class T> class MyClass {
private:
T val_;
public:
MyClass();
MyClass(const T &);
~MyClass();
T operator()(const T &);
};
// Tell compiler that MyClass has been instantiated somewhere for float and
// double
extern template class MyClass<double>;
extern template class MyClass<float>;
// Tell compiler that somefunc has been instantiated somewhere for float and
// double
template <class T> T somefunc(const T &, void *&);
extern template double somefunc(const double &, void *&);
extern template float somefunc(const float &, void *&);
// Tell compiler that deletehandle is available somewhere
extern void deletehandle(void *);
// Tell compiler that below function names are available in C-symbols
extern "C" {
double d_somefunc(const double, void **);
double f_somefunc(const float, void **);
void delhandle(void *);
}
#endif
/* external.cpp file */
#include "external.hpp"
#include <iostream>
#include <map>
#include <memory>
template <class T> MyClass<T>::MyClass() = default;
template <class T> MyClass<T>::MyClass(const T &val) : val_{val} {}
template <class T> MyClass<T>::~MyClass() { std::cout << "dtor called.\n"; }
template <class T> T MyClass<T>::operator()(const T &val) {
T res{val_ * val};
val_ = val;
return res;
}
// Tell compiler to instantiate MyClass here for float and double
template class MyClass<double>;
template class MyClass<float>;
template <class T> void myclassdeleter(void *ptr) {
if (ptr != nullptr)
delete static_cast<T *>(ptr);
}
std::map<void *, std::unique_ptr<void, void (*)(void *)>> myptrs;
template <class T> T somefunc(const T &input, void *&handle) {
auto search = myptrs.find(handle);
if (search == myptrs.end()) {
std::unique_ptr<void, void (*)(void *)> up{new MyClass<T>{T{0}},
&myclassdeleter<MyClass<T>>};
handle = up.get();
myptrs.emplace(handle, std::move(up));
}
std::cout << "myptrs.size(): " << myptrs.size() << '\n';
auto objptr = static_cast<MyClass<T> *>(handle);
auto res = objptr->operator()(input);
return res;
}
// Tell compiler to instantiate somefunc here for float and double
template double somefunc(const double &, void *&);
template float somefunc(const float &, void *&);
void deletehandle(void *handle) {
myptrs.erase(handle);
std::cout << "myptrs.size(): " << myptrs.size() << '\n';
}
// Export below functions in C-symbols
extern "C" {
double d_somefunc(const double input, void **handle) {
return somefunc(input, *handle);
}
double f_somefunc(const float input, void **handle) {
return somefunc(input, *handle);
}
void delhandle(void *handle) { return deletehandle(handle); }
}