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

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