Cconvert and unsafe_convert with immutable struct containing a pointer

Yes, except I think for your example the GC.@preserve is unnecessary - you should be able to just do

struct DenseMatrix # immutable!
    rowCount::Cint
    columnCount::Cint
    data::Ptr{Cdouble}
end

Base.cconvert(::Type{DenseMatrix}, m::Matrix{Float64}) = m

function Base.unsafe_convert(::Type{DenseMatrix}, m::Matrix{Float64})
    return DenseMatrix(size(m, 1), size(m, 2), pointer(m))
end

function CWrapper(arg1::Matrix{Float64})
    @ccall lib.fname(arg1::DenseMatrix)::<rettype>
end

since the DenseMatrix will be passed by-value (i.e. as a copy) anyway (it’s isbits after all) and itself doesn’t need to be GC preserved, as I understand it. Just returning m from cconvert already @preserves the matrix itself:

julia> isbitstype(DenseMatrix)
true

shell> cat mwe.c
#include <math.h>

typedef struct {
  int rowCount;
  int columnCount;
  double* data;
} DenseMatrix;

double foo(DenseMatrix foo) {
  if (foo.columnCount <= 0)
    return NAN;
  if (foo.rowCount <= 0)
    return INFINITY;

  int idxa = foo.columnCount - 1;
  int idxb = foo.rowCount - 1;
  return foo.data[idxb*(foo.columnCount) + idxa];
}

julia> data = rand(Float64, 15, 37)
15×37 Matrix{Float64}:
 0.599364    0.22981    0.96945    …  0.344901   0.416426   0.192333
 0.0278234   0.0835467  0.460888      0.665467   0.825422   0.430733
 0.365078    0.155395   0.0188155     0.968414   0.797662   0.628757
 0.457193    0.256214   0.374096      0.0370763  0.261907   0.141956
 0.181229    0.0202176  0.0417381     0.595971   0.425398   0.873739
 0.166689    0.799661   0.30146    …  0.379971   0.0946295  0.709757
 0.602595    0.0282483  0.327141      0.211264   0.93809    0.991113
 0.446278    0.25726    0.617319      0.422344   0.473222   0.343918
 0.331866    0.346359   0.464125      0.649524   0.568934   0.910309
 0.00314365  0.0951955  0.523954      0.366409   0.239934   0.273698
 0.625282    0.484357   0.646132   …  0.0714788  0.949366   0.181078
 0.446451    0.203708   0.637352      0.57657    0.238058   0.800698
 0.528139    0.915547   0.15079       0.561895   0.812107   0.940406
 0.398491    0.664749   0.708104      0.979059   0.0340735  0.347522
 0.965296    0.236917   0.781048      0.507677   0.866314   0.377035

julia> CWrapper(data)
0.3770354806819668

julia> CWrapper(data) === last(data)
true

julia> function foo(s1, s2)
           data = rand(Float64, s1, s2)
           CWrapper(data) == last(data)
       end
foo (generic function with 1 method)


julia> @allocated foo(10, 15)
1296

julia> 10*15*8 # size of the matrix allocated in foo, plus some overhead for the `Vector` struct etc
1200

One thing you will have to be careful about is row- vs column-majorness, since Julia is column major and C-libraries usually do row-major.