Platform · Lesson 24.1

Extern

Source
Call a function from a system library by declaring it with extern and naming its library with #Link.
Windows only.
The functions in this lesson come from Kernel32.dll, which only Windows has. On Linux, macOS or FreeBSD the build stops with an #Error that says so.

Not every function a program calls was written in Rux. The operating system offers thousands of its own — to ask the time, open a window, find out which process you are — compiled long ago and shipped in shared libraries: .dll files on Windows, .so on Linux and FreeBSD, .dylib on macOS. The standard packages call them for you all the time. This lesson calls two directly.

extern declares a function that lives in one of those libraries: its name, its parameters and its result, with no body, because the body is already in the library. #Link names the library, and the linker connects the two.

Declaring a foreign function

// One `#Link` on a block applies to every declaration inside it.
#Link("Kernel32.dll")
extern {
    // DWORD GetCurrentProcessId(void): the number Windows gave this running program.
    func GetCurrentProcessId() -> uint32;

    // int lstrlenA(const char *text): counts bytes up to the first zero byte.
    func lstrlenA(text: *char8) -> int32;
}

The comments show each function as Microsoft's documentation writes it, in C. Writing the Rux declaration means translating that line, type by type:

In the C documentationMeansIn Rux
DWORDa 32-bit unsigned numberuint32
inta 32-bit signed numberint32
const char *the address of bytes it will not change*char8
void (parameters)no parameters()

An extern { } block holds as many declarations as you like, and one #Link above it covers them all. A single function can be declared on its own too: #Link("Kernel32.dll") on one line, extern func GetCurrentProcessId() -> uint32; on the next.

Once declared, a foreign function is called like any other — nothing at the call site says it is unusual:

PrintLine("process id   {}", GetCurrentProcessId());

The compiler takes it on trust

The compiler cannot look inside Kernel32.dll to check what you wrote. It takes the declaration on trust: declare lstrlenA with an int64 parameter, and the call goes ahead and hands the function the wrong bytes. Nothing reports it; the program just behaves strangely. So copy a declaration carefully from the library's documentation — the Windows API pages of this site list the declarations the Windows package already uses.

On Windows the linker does check one thing: that the library really exports a function of that name. A misspelt name stops the build at the linking step instead of failing on the user's machine.

Strings that end in a zero byte

C functions do not receive a length with a string. They find the end by looking for a zero byte. A Rux string literal keeps one after its last character — .length does not count it — so .data, the address of the first byte, is exactly what a C function expects:

let word = "extern";
PrintLine("Rux length   {}", word.length);
PrintLine("lstrlenA     {}", lstrlenA(word.data));

Both lines print 6: Rux counted the characters it stores, lstrlenA counted bytes up to the zero. That is true of string literals. A slice cut out of the middle of a string has no zero byte after it, and a C function handed its .data would read straight past the end.

Building for the right system

Kernel32.dll exists only on Windows, so the declarations are wrapped in a when on the target, and every other system gets a compile error that explains itself:

when #target.os {
    .Windows => {
        // … the extern block above …
    },
    else => #Error("This lesson calls Kernel32.dll, so it builds for Windows only")
}

Run rux build --target linux-x86_64 on any machine and the build stops with exactly that message — far better than a program that builds and then cannot find its library.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Not every function a program calls was written in Rux. The operating system offers thousands
// of its own, compiled long ago and shipped in shared libraries. `extern` declares one of them:
// its name, its parameters and its result, with no body, because the body is already in the
// library. `#Link` names that library, and the linker connects the two.
//
// The compiler cannot look inside the library to check the declaration. It takes what you wrote
// on trust, so a wrong parameter type is not a compile error: the call goes ahead and passes the
// wrong bytes. Copy a declaration carefully from the library's documentation.
//
// The functions below live in Kernel32.dll, which only Windows has. `when #target.os` keeps
// them out of every other build, and `#Error` explains why such a build stops.
import Core::{ #Error, #target };
import Io::PrintLine;

when #target.os {
    .Windows => {
        // One `#Link` on a block applies to every declaration inside it.
        #Link("Kernel32.dll")
        extern {
            // DWORD GetCurrentProcessId(void): the number Windows gave this running program.
            func GetCurrentProcessId() -> uint32;

            // int lstrlenA(const char *text): counts bytes up to the first zero byte.
            func lstrlenA(text: *char8) -> int32;
        }
    },
    else => #Error("This lesson calls Kernel32.dll, so it builds for Windows only")
}

func Main() -> int {
    // Called like any other function. Neither of these can fail, which is why they were
    // chosen: most system functions can, and the next lesson checks for that.
    PrintLine("process id   {}", GetCurrentProcessId());

    // C functions find the end of a string by looking for a zero byte. A Rux string literal
    // keeps one after its last character, though `.length` does not count it, so `.data`, the
    // address of the first byte, is exactly what a C function expects.
    let word = "extern";
    PrintLine("Rux length   {}", word.length);
    PrintLine("lstrlenA     {}", lstrlenA(word.data));
    return 0;
}

Besides Io, its Rux.toml lists Core under [Dependencies].

Run it

cd Examples/Platform/Extern
rux run
process id   6640
Rux length   6
lstrlenA     6

The process id is different on every run.

Common mistakes

Passing a Rux string where C expects an address.
lstrlenA(word) fails with error: argument 1 to 'lstrlenA' has type 'char8[..]', but parameter 'text' requires '*char8'. Pass word.data.
Forgetting #Link.
Without it the compiler does not know where the function lives: error: extern function 'GetCurrentProcessId' must specify a source DLL via #Link("dll.dll").
Misspelling the function's name.
Library names are exact, capitals included. GetCurrentProcessID stops the build when linking: error: cannot link PE/COFF executable 'Extern': import function 'GetCurrentProcessID' was not found in DLL 'Kernel32.dll'.
A declaration that does not match the library.
A wrong parameter or result type is not an error at all: the compiler believes the declaration, and the function receives or returns the wrong bytes. Check every type against the documentation.

Try it yourself

  1. Add GetCurrentThreadId, which also takes nothing and returns a DWORD, to the extern block and print it.
  2. Declare GetTickCount64() -> uint64 and Sleep(milliseconds: uint32) from the same library. Read the tick count, sleep for 500 milliseconds, and print how long the sleep really took.
  3. Run rux build --target linux-x86_64 and read the message.

Learn more