Properly using `finalizer`, `ccall`, `cconvert` and `unsafe_convert`

I have problems when using finalizers 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

  1. if handle (the second argument) points to an already allocated (by the C++ library) object of some type MyClass,
  2. if not, they create a new object on the heap and change handle’s value to point to the corresponding object’s address, and,
  3. they call the object’s operator() on the val.

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 :slight_smile:

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); }
}
1 Like

I see no reason to use a Ref here.

This is not allowed

Hey @yuyichao. Thank you for your prompt answer.

I think I need to use some sort of Ref there, please check the external.cpp file above in the edited version.

So, you think the println statement was the problem, then? I will try without them. What are the allowed operations, then?

No, you only need to mutate the pointer, which doesn’t need a Ref. It’s also an abstract type FWIW.

Anything that does not violate task switch not allowed from inside gc finalizer.

Could you provide a small example/hint on how to achieve this? I mean, should I need a change in the C API or in Julia? I have tried before Ptr{Ptr{Void}} and my Julia crashed. Which one is also an abstract type? I am sorry, I cannot follow since these are not covered in docs, or simply I could not understand from what I read there.

Again, I could not find any list of functions/operations that result in task switching.

Just use a ::Ptr{Void} as the field type. You should just figure out how you would write such an interface code in C and just map that to julia.
The use of ccall here is also invalid since julia is allowed to free m before any of the ccall. So you have to define unsafe_convert and cconvert methods that does the correct convertion with defined GC behavior. You can do any pointer arithmetic you want in the unsafe_convert after taking the address of the object with pointer_to_objref. See the doc for cconvert and unsafe_convert which are mentioned here

Ref{Ptr{Void}} Ref https://docs.julialang.org/en/latest/manual/types/#Abstract-Types-1. In generl, your code seems to be having a lot of type instability, which isn’t necessarily an issue if you don’t care about performance.

There isn’t such a list, in general though any IO function will have such an effect.

Thank you, again, for your answer.

OK. Your concern with Ref inside my Julia struct was due to the struct’s having an abstract field, right? I mean, the type instability, and hence the performance degradation, was due to the abstract field, or?

In C, I cannot pass void * x to my function if I were to change x to point to a different address. In the end, x gets copied, right? At least I couldn’t achieve it unless I was using void ** and passing x’s address in the C code (similarly, void *& x in the C++ version, to indicate pass-by-ref).

Which of ccall is invalid? The one in the finalizer, i.e., destruct!? Should I put gc_enable(false); gc_enable(true) guards inside destruct! to protect ccall?

I have also tried using the below code, which did not succeed, either:

mutable struct MyType{T<:Real}
  val::T
  handle::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, Ptr{Void}(0))
  finalizer(obj, destruct!)
  return obj
end

function destruct!(m::MyType)
  ccall(delhandle, Void, (Ptr{Void},), handle(m))
end

function (m::MyType)()
  return ccall(funhandle, Cdouble, (Cdouble, Ptr{Ptr{Void}}), value(m),
    pointer_from_objref(m.handle))
end

And, by the way, pointer_from_objref(m.handle) in this case seems to return different addresses every time I run the command.

Thank you for the information.

EDIT. The below code for the object call seems to solve my problem, though I am not sure if it is OK/valid to write like that:

# struct definition and others same as above...

function (m::MyType)()
  objref = Ref{Ptr{Void}}(handle(m))
  res = ccall(funhandle, Cdouble, (Cdouble, Ref{Ptr{Void}}), value(m), objref)
  m.handle = objref[]
  return res
end

This is the issue with abstract field, yes.

In C you’ll of course have a void *x; field, and you’ll pass it as &obj->x. The same is easily doable in julia with properly defined unsafe_convert.

All of them, actually the finalizer one might be the only valid one if it is only used as a finalizer since the GC will root the argument for you.

Never, ever do that.

You should put the conversion in unsafe_convert instead. Your ccall MUST reads ccall(......, m), not m.handle , not handle(m), not anything other than m.

1 Like

Thank you for your message. I think I have improved upon the first version by taking into account your warnings:

libhandle = Libdl.dlopen(joinpath(pwd(), "libexternal.so"))
funhandle = Libdl.dlsym(libhandle, :d_somefunc)
delhandle = Libdl.dlsym(libhandle, :delhandle)

import Base: cconvert, unsafe_convert

mutable struct MyType{T<:Real}
  val::T
  handle::Ptr{Void}
end

value(m::MyType)              = m.val
value!(m::MyType, val::Real)  = (m.val = val)

cconvert(t::Type{Cdouble}, m::MyType)         = cconvert(t, value(m))
unsafe_convert(t::Type{Ptr{Void}}, m::MyType) = m.handle

function destruct!(m::MyType)
  ccall(delhandle, Void, (Ptr{Void},), m)
end

function (::Type{MyType})(val::Real)
  obj = MyType{typeof(val)}(val, Ptr{Void}(0))
  finalizer(obj, destruct!)
  return obj
end

function (m::MyType)()
  handleref = Ref{Ptr{Void}}(m.handle)
  res = ccall(funhandle, Cdouble, (Cdouble, Ref{Ptr{Void}}), m, handleref)
  m.handle = handleref[]
  return res
end

m = MyType(1)
m()
value!(m, 5)
m()
value!(m, 7)
m()

# Run below for 4 times
for k in 1:4
  m = MyType(1)
  m()
  value!(m, 7)
  m()
end

gc()

Now, ccall does not seem to have obj.field parts, nor does my unsafe_convert have any expression, as is forbidden in the documentation.

The only problem, apparently, is that there is no difference between Ref{Ptr{Void}} and Ptr{Void} in Julia. Ref{Ptr{Void}} is also calling convert(::Type{Ptr{Void}}, x). For this reason, I needed to encapsulate m.handle inside Base.RefValue inside the functor.

Still I cannot understand your point above — forgive me. I am having the problem:

double somefunc(const double val, void *x) {
  double result;
  // do something with x->func(val) and set result to something
  // change x to point to a different object
  return result;
}

Since I cannot change the argument x’s value, but only its copy inside the function, I need void ** here. Then, I can simply

double somefunc(const double val, void **x) {
  double result;
  // do something with (*x)->func(val) and set result to something
  // change x to point to a different object by
  // *x = new_address;
  return result;
}

This is not forbidden, the forbidden thing is returning address of anything you allocated in the function.

That’s not true, see below.

Yes, your function will take a void** that’s fine, but if you want to mutate a void* in a C struct, you don’t define that field as void**, you’ll just define it as void*. And you’ll use it as

struct mytype {
    double v;
    void *ptr;
};

// and some code in a function
mytype *obj;
// ...
somefunction(obj->v, &obj->ptr);

And you can write a exact equivalent in julia.

No this is not necessary.

Also

is fine but also not necessary. The validity of the ::Cdouble doesn’t rely on the liveness of the ::MyType. It’s certainly valid code though, just a little over-complicated.

The direct (and invalid!) way to implement the c++ code above in julia would simply be

ccall(somefunction, Cdouble, (Cdouble, Ptr{Ptr{Void}}), obj.v, pointer_to_objref(obj) + fieldoffset(typeof(obj), 2))

The only invalid part of the above code is that pointer_to_objref(obj) + fieldoffset(typeof(obj), 2) will not keep obj alive during the call. And the fix would be to somehow keep it alive. This can be acomplished either with @gc_preserve on 0.7 like

Base.@gc_preserve obj ccall(somefunction, Cdouble, (Cdouble, Ptr{Ptr{Void}}), obj.v, pointer_to_objref(obj) + fieldoffset(typeof(obj), 2))

which will be the preferred way if the convertion is callsite specific. On 0.6 where there’s no @gc_preserve or if this call convertion is used multiple times, it is preferred to use the unsafe_convert/cconvert and you can do this simply by putting that unsafe convert in, well, unsafe_convert, sth like.

Base.unsafe_convert(::Type{Ptr{Ptr{Void}}}, obj::MyType) = Ptr{Ptr{Void}}(pointer_to_objref(obj) + fieldoffset(typeof(obj), 2))

And simply do

ccall(somefunction, Cdouble, (Cdouble, Ptr{Ptr{Void}}), obj.v, obj)

The use of either Ref{Ptr{Void}} or Ptr{Ptr{Void}} is valid and doesn’t really matter. In fact, any Ref or Ptr type should work. I usually prefer Ptr though since Ref has special meaning and it is better to not mess with it.

And I’ll also add that I can see how this is a failed attempt at translating the C code I translated above. The important thing to note here about the difference between C and julia is that there’s no C reference in julia. Even though it is not called so in C, obj.field produces a lvalue reference which is why & on it gives you the address inside obj. OTOH, obj.field always produce a rvalue in julia, or in another word, local variable assignment is never significant, you can insert as many of them as you want in the code and they’ll never make any difference in semantics. This means that pointer_from_objref(m.handle) corresponds to

{
    void *handle = m->handle;
    return &handle;
}

and it should be easy to see why it doesn’t give you the result you want. That’s also why you need to explicitly take the address of m in order to get the address of handle field in m.

1 Like

Also note that this is not forbidden just for unsafe_convert but forbidden in any function (ok, well, it’s not really forbidden but it is just undefined behavior if the caller want to use the pointer to load/store anything. It’s obviously fine to return any pointer you feel like as long as you promise not to use that pointer for anything bad).

In fact, there is less restriction on unsafe_convert than any other functions. It is the only function you can return an address derived from the argument since the caller must keep the arguments valid if it want to use the returned pointer. You can of course require your own functions to have a similar semantic (i.e. restriction on the caller) but unsafe_convert is the only Base API that does.

1 Like

Wow! Thank you for all the explanations — this was a bit trickier than I had expected.

I would like to clarify a couple of things for myself, by repeating what you had written, if you don’t mind.

Sorry for my misuse of the word forbidden. I meant to refer to the following paragraph in the documentation for Base.unsafe_convert:


Be careful to ensure that a Julia reference to x exists as long as the result of this function will be used. Accordingly, the argument x to this function should never be an expression, only a variable name or field reference. For example, x=a.b.c is acceptable, but x=[a,b,c] is not.

This piece of information was the reason I was trying to avoid using any expression but the object itself or its field names via obj.fieldname above. But now, I see that even if Ptr{Ptr{Void}}(pointer_to_objref(obj) + fieldoffset(typeof(obj), 2)) creates a temporary, the temporary is simply the address of something which will stay alive during the call.

Thank you once more for clarification.

Regarding Ref{Ptr{Void}}, I simply wanted to say that I could not use the method overload cconvert(::Type{Ref{Ptr{Void}}}, x) when there was cconvert(::Type{Ptr{Void}}, x) present. But, yet again, what I should have done was to use Ptr{Ptr{Void}} as you have pointed out already.

And this is because I am simply creating a temporary, directly inside ccall, which does not guarantee anything related to the actual object’s staying live or not, right? Basically, indirection must be avoided.

The above code will be valid, provided that I have at least one obj passed directly inside ccall, right?

Uhm… Again, as I said above, Ref{Ptr{Void}} does not resolve to Ref{Ptr{Void}} overload of unsafe_convert. For this reason, I needed to use Ptr{Ptr{Void}} in my overload (see below).

Moreover, what do you mean by special meaning of Ref? Is it only that Ref is used to indicate the variable is in gc’s responsibility as opposed to Ptrs being the C library’s, or is there something else? To avoid any misuse of wording, as I had already done with “forbidden”, I refer to the following statements:

For C code accepting pointers, Ref{T} should generally be used for the types of input arguments, allowing the use of pointers to memory managed by either Julia or C through the implicit call to Base.cconvert. In contrast, pointers returned by the C function called should be declared to be of output type Ptr{T}, reflecting that the memory pointed to is managed by C only.

I slightly disagree with your comment on C and lvalue reference, but your explanation makes perfect sense. I think the dot operator in Julia has a different meaning from that in Julia. But thinking, roughly, that pointer_from_objref(m.handle) actually means pointer_from_objref(getfield(m, :handle)) clarifies the observation I had. Again, I was reaching for the address of a temporary.

Thank you very much for your time and explanations. Below is the correct version, I hope, for the last time:

libhandle = Libdl.dlopen(joinpath(pwd(), "libexternal.so"))
funhandle = Libdl.dlsym(libhandle, :d_somefunc)
delhandle = Libdl.dlsym(libhandle, :delhandle)

import Base: cconvert, unsafe_convert

mutable struct MyType{T<:Real}
  val::T
  handle::Ptr{Void}
end

value(m::MyType)              = m.val
value!(m::MyType, val::Real)  = (m.val = val)

unsafe_convert(t::Type{Ptr{Void}}, m::MyType)       = m.handle
unsafe_convert(t::Type{Ptr{Ptr{Void}}}, m::MyType)  =
    Ptr{Ptr{Void}}(pointer_from_objref(m) + fieldoffset(typeof(m), 2))

function destruct!(m::MyType)
  ccall(delhandle, Void, (Ptr{Void},), m)
end

function (::Type{MyType})(val::Real)
  obj = MyType{typeof(val)}(val, Ptr{Void}(0))
  finalizer(obj, destruct!)
  return obj
end

function (m::MyType)()
  res = ccall(funhandle, Cdouble, (Cdouble, Ptr{Ptr{Void}}), value(m), m)
end

m = MyType(1)
m()
value!(m, 5)
m()
value!(m, 7)
m()

# Run below for 4 times
for k in 1:4
  m = MyType(1)
  m()
  value!(m, 7)
  m()
end

gc()

I mean, it works as expected, finally! It is also easier to parse if one knows how to write the counterpart in the C version.

1 Like

As I said, if you just figure out how you would write the whole thing in C, it shouldn’t be too hard to translate it to julia. And with a few tweaks you can make it valid julia.

Forbidden isn’t misused afaik. Though that doc is misleading. It’s trying to say that unsafe_convert(T, a.b.c) is allowed while unsafe_convert(T, [a, b, c]) isn’t though really none is allowed on it’s own…

Not sure what you mean by temporary here. Every expression creates “temporary” and that’s not a problem. It’s not even a issue if the expression allocates. The only issue is if the expression takes the address of an object and uses the address without making sure the object is always alive, whether or not that object is a “temporary”. Semantically even this expression may allocate, it’s just that the optimizer should be able to do a good job almost all of the cases. And even if this expression allocates the pointer on the heap, it is actually not necessary to keep that alive since the value will be used, not the heap allocated Ptr object.

That’s what I thought you mean and that shouldn’t be the case…

That’s not the issue. The issue is that with a few exceptions, for any expression f(a, b, g(c, h(d))) (the function call is just for illustration can be replaced with any other expression like operators and getfield (which are really special function calls…)) they are semantically equivalent to

t1 = h(d)
t2 = g(c, t1)
t3 = f(a, b, t2)

And there’s no guarantee that any of the variable is alive other than where it appears in the code. E.g. d may be dead during the second or third expression and a may be dead during the first two expressions!!! (The latter can happen due to compiler optimization especially dead code elimination). This is also very different from C++ where the lifetime of t1 and t2 are guaranteed beforee the whole expression finishes. So it’s invalid because it’s using the address of obj but ccall(somefunction, Cdouble, (Cdouble, Ptr{Ptr{Void}}), obj.v, pointer_to_objref(obj) + fieldoffset(typeof(obj), 2)) doesn’t tell the compiler that it should preserve the obj during the ccall, which is also why that’s the only thing that need fixing. In another word, pointer_to_objref(obj) + fieldoffset(typeof(obj), 2) by itself does not create any problematic temporaries (i.e. it doesn’t take address of any temporaries). It’s just that it returns a value whose validity is tied to the liveness of obj and the user need to be aware of that.

The above code should already be valid and the reason for that is obj is used as the return value of cconvert for one of the argument. What you said is kind of true since breaking that requires a invalid cconvert and with a valid cconvert what you said should be true.

No, not at all, absolutely not. The mention of that in the doc (what you linked and quoted below) seems to be confusing a large number of people…
See Use Ref to denote GC-managed memory. by maleadt · Pull Request #56 · JuliaGPU/CUDAdrv.jl · GitHub. Basically it doesn’t have any special meaning to the GC, not at all. It’s treated exactly the same as Ptr, just with a few methods defined differently to give it the special meaning mentioned below.

The special meaning is the special cconvert rule it has to pass a value by reference “implicitly” like ccall(..., (..., Ref{Int}, ...), ..., 1, ...) You shouldn’t overload unsafe_convert/cconvert(::Type{Ref{T}}, ::T2) where T2 can be reasonably converted to T. Anything else should be fine though I see no reason to do so… Note that you do need to overwrite both cconvert and unsafe_convert though since the special cconvert behavior means that it doesn’t have a no-op default cconvert. I don’t see a reason to use Ref here though and since whether T2 can be reasonably converted to T is pretty fuzzy it’s better not to mess with it.

Sure, I’m just using C++ terminology which generalizes the semantic of these sub expression results. It’s definately a lvalue reference in c++, in which case even &handle(m) can be valid.

Yep, looks correct. The other type instability I mentioned is

You should just use (:d_somefunc, libpath) in ccall directly or make them const or Ref{Ptr{Void}}. What you have is perfectly correct though, just slightly inefficient.

I can confirm that this is not working. Maybe I should open an issue then on GitHub. I might be wrongly implementing this, but I have tried (not valid anymore, see below)

unsafe_convert(t::Type{Ptr{Void}}, m::MyType)       = m.handle
unsafe_convert(t::Type{Ref{Ptr{Void}}}, m::MyType)  =
    Ref{Ptr{Void}}(pointer_from_objref(m) + fieldoffset(typeof(m), 2))

as well as the Ptr{Ptr{Void}} casted return type for the second overload above (with the proper ccall change), and I have always got

MethodError: Cannot `convert` an object of type MyType{Int64} to an object of type Ptr{Void}
This may have arisen from a call to the constructor Ptr{Void}(...),
since type constructors fall back to convert methods.

Anyways… For me, this is not a problem anymore, and I do think that the Ptr{Ptr{Void}} solution should be the preferred way. That’s just more reasonable from its C counterpart.

EDIT & CORRECTION. Actually, following this comment, I think I have found the bug in my above code snippet. Since, as you pointed out, I have to overload both of them, I need the below code snippet (assuming T = Ptr{Void} and T2 = MyType) to make things work correctly:

unsafe_convert(t::Type{Ptr{Void}}, m::MyType)       = m.handle
cconvert(t::Type{Ref{Ptr{Void}}}, m::MyType)        = m
# no reasonable conversion above. hence, define he conversion to be
# the object itself. then, let the unsafe convert do its job below
unsafe_convert(t::Type{Ref{Ptr{Void}}}, m::MyType)  =
    Ptr{Ptr{Void}}(pointer_from_objref(m) + fieldoffset(typeof(m), 2))

Prior to your comment above, I was getting the method error I have mentioned in this message. Now I understand the reason: lack of no-op cconvert.

For all the help, thank you very much, once more!