Extern
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 documentation | Means | In Rux |
|---|---|---|
DWORD | a 32-bit unsigned number | uint32 |
int | a 32-bit signed number | int32 |
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.
// 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
lstrlenA(word) fails with error: argument 1 to 'lstrlenA' has type 'char8[..]', but parameter 'text' requires '*char8'. Pass word.data.#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").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 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
- Add
GetCurrentThreadId, which also takes nothing and returns aDWORD, to theexternblock and print it. - Declare
GetTickCount64() -> uint64andSleep(milliseconds: uint32)from the same library. Read the tick count, sleep for 500 milliseconds, and print how long the sleep really took. - Run
rux build --target linux-x86_64and read the message.
Learn more
- Foreign function interface, Extern declarations and Linking libraries in the Rux Reference
- Link — the attribute in full, including a different symbol name
GetCurrentProcessIdand the rest of the Windows package- Shared library — building a library of your own that
externcan call - C interop — the next lesson, which calls the C runtime on every system
Overview
Step outside Rux to the operating system and the processor. Five lessons on extern and #Link, the C runtime and its types, calling conventions with #Abi, and function bodies written in x86-64 and AArch64 assembly.
24.2 C interop
Call the C runtime through the C package. The program uses C's own types, turns an opaque pointer into a typed one, reads a C struct, makes a variadic call and checks every result for failure.