Wrote a macro to make ccall look a little more like Julia. Need an opinion on the return type placement

I wrote this macro that lets you put the types of the C arguments in the places where you’d put type annotations in a Julia function definition. Initially, I had the return type at the end of the function, as you would have in a Julia declaration, but now I’ve moved them up front, because they are closer to the variable that gets the value that way.

# currently
err = @ccall Cint mkfifo(pathname::Cstring, mode::Cuint)

const glib = "libglib-2.0"
cstr = @ccall Cstring glib.g_uri_escape_string(str::Cstring, allow::Cstring, true::Cint)

# previously
err = @ccall mkfifo(pathname::Cstring, mode::Cuint)::Cint

const glib = "libglib-2.0"
cstr = @ccall glib.g_uri_escape_string(str::Cstring, allow::Cstring, true::Cint)::Cstring

Anyone got an opinion on which is nicer to read?

14 Likes

I think putting it after the ) is best. This is almost identical to something I proposed once :slight_smile:

8 Likes

Thanks for the feedback! To the end it goes.

Early version of the here, if anyone wants to look at it or copy it (it’s trivial to implement it yourself, of course):

1 Like

Do you remember why it never ended up happening?

I’d rather like to have this @ccall in Base, it seems a lot more readable.

4 Likes

16 posts were split to a new topic: Ccall macros for controlling SIGINT handling

Just because no one ever did it and it’s not strictly necessary. ccall probably should have been a macro in the first place but it predates macros in the language.

Eh, should I do a PR? :man_shrugging:

I don’t know what the system is for contributing to Julia.

2 Likes

Sure! Although it’s probably best to make a CcallMacro.jl package first (even unregistered) and get it all working to your satisfaction. Building Julia just to test this will get annoying really quickly.

3 Likes

Hi @StefanKarpinski,

One question: can’t we, while doing minor developments to stdlib for example, download the latest nightly build and create a sysimg with PackageCompiler?

The more I think about it, the less I like it. The way it is now, everything in the generated code is clearly shown in the input code. I don’t like the idea that a macro with the same name as a built-in function would have different behavior. I’ll write another macro that does what you suggest. I’m thinking @safecall, unless someone has a better name. Or maybe @callc? Not as obviously what it’s changing, but it’s shorter and has the benefit of not “lying” by using the name of a built-in but changing the behavior.

Regarding the issue you referenced, it might be interesting to see if there is a way to statically determine if code is being run in a separate thread or task and see if there is a way to alter the semantics of ccall on that basis. If it were possible, @unsafe_ccall would only be semantically different in concurrent code.

Of course, I don’t know anything about the compiler, and I expect statically analysing for concurrency is probably rather tricky (on the other hand, Julia’s current ability to statically analyse code is already bordering on the occult, so what do I know?)

There’s precedence. For example, @show vs show. It seems natural (to me) that the macro does approximately the same thing as the function, but with less typing.

1 Like

Yep. Same with @view, @eval and Meta.@dump I’m sure there are more.

Alright made the repo:

I suppose I need to add tests and stuff now. Also of note, I implemented something to do with varargs, but it doesn’t work.

I implemented it so this:

@ccall printf("%d%d%d\n"::Cstring, a::Clong...)::Cint

becomes

ccall(:printf, Cint, (Cstring, Clong...), "%d%d%d\n", a...)

But that apparently isn’t how ccall works.

I don’t actually know how this is supposed to look.

3 Likes

printf and other varargs functions aren’t called the way normal C functions are. IIRC, each compiler is free to implement the varargs calling convention differently, although in practice I think there’s a fairly standard way to do it (I don’t recall what it is, but I think it involves an implicit **void argument terminated by a NULL pointer (kind of the obvious way you’d do it manually). You would need to emulate the C varargs calling convention in order to call printf correctly.

printf and other varargs functions aren’t called the way normal C functions are

That’s not generally true. They aren’t called like a normal ANSI-C function because they’re typically required to be a normal K&R-C function. (But then those are often required to be the same, so you can usually get away with ignoring the difference.) In practice, some APIs that provide a ... varargs function use a void** implementation underneath (and expose that to users too) because the vararg itself can actually be fairly difficult to work with in the general case since it has a opaque representation.

Are you saying that you can call a varargs function through ccall like this and expect it to work?

I’ve successfully used printf with ccall before, just not with argument expansion on the Julia side. The message makes it look like ccall is already doing something macro-y (i.e. it seems not to expand arg…)

This works some of the time, but is not portable or reliable. For example, this works for me on MacOS:

julia> ccall(:printf, Cint, (Cstring,), "Hello world!\n")
Hello world!
13

julia> ccall(:printf, Cint, (Cstring, Cdouble), "A number %g.\n", 3.14159)
A number 3.14159.
18

but on Windows I get:

julia> ccall(:printf, Cint, (Cstring,), "Hello world!\n")
Hello world!
13

julia> ccall(:printf, Cint, (Cstring, Cdouble), "A number %g.\n", 3.14159)
A number 0.
12

(Cstring, Cdouble...) for both cases.

True, this works! But I have to admit I don’t know why. Could you explain please?