Skip to content

slices.md L109-119 'memcpy' example is broken on three independent points; extern.md gives no guidance on linking against libraries #22

@davidfrogley

Description

@davidfrogley

Summary

Web/src/docs/types/slices.md:109-119 shows an FFI example using memcpy:

extern func memcpy(dst: *opaque, src: *opaque, n: uint) -> *opaque;

let src = [1, 2, 3, 4];
let dst: uint8[4];
memcpy(dst.data, src.data, src.length);

The example is broken on at least three independent points, plus a fourth that surfaces the moment a user tries to fix the others. None of these problems are addressed by the surrounding text or by extern.md.

The four defects

  1. extern declaration has no library binding

memcpy lives in msvcrt.dll on Windows / libc on POSIX. The doc's bare extern func memcpy(...) declaration does not tell the linker where to find the symbol. On Windows the build fails to resolve memcpy unless the user adds:

@[Import(lib: "msvcrt.dll")]
extern func memcpy(dst: *opaque, src: *opaque, n: uint) -> *opaque;

extern.md (the dedicated FFI doc) has no mention of @[Import] at all — it shows only bare extern declarations as if that's sufficient:

extern func malloc(size: uint) -> *opaque;
extern func free(ptr: *opaque);
extern func sin(angle: float) -> float;

The @[Import(lib: ...)] mechanism is used in Web/src/blog/language-without-llvm.md:127 and is presumably how stdlib modules like Std::Memory reach kernel32.dll / msvcrt.dll, but it's nowhere in the user-facing FFI documentation. Users have no documented path from "I want to call libc / Win32" to a working program.

  1. let src = [1, 2, 3, 4] is int64[], not uint8[]

The doc later does memcpy(dst.data, src.data, src.length) where dst: uint8[4]. The two slices are not type-compatible — src's element type is int64 (8 bytes per element), dst's is uint8 (1 byte). Even through *opaque, the byte counts and layouts don't line up.

Hir.cpp:682 types unsuffixed integer literals as int (= int64 on x86-64). Nothing in the doc text alerts the reader that [1, 2, 3, 4] won't be uint8[] by default, or that suffixes like 1u8 are needed to match dst's element type.

  1. let dst: uint8[4] doesn't compile

let requires an initializer (Sema.cpp:2060), so the bare declaration errors with "immutable variable requires an initializer." Already filed separately; mentioning here because it's part of why a user copy-pasting this example gets stuck before they can even reach defects 1 and 2.

  1. src.length is the count of elements, not bytes

Even if a user fixes defects 1-3 (add @[Import], switch dst to a matching element type, change let to var), the call still produces wrong values because src.length == 4 regardless of element width. memcpy's third parameter is a byte count. For int64[4], the user needs src.length * sizeof(int64) = 32 bytes, not 4.

The user's actual failing case after fixing 1-3 (using int[4] for both): memcpy copies 4 bytes out of 32, leaving 28 bytes of dst uninitialized. Printed values like 4294967297 (= 0x100000001) are exactly what you'd expect: the first 4 bytes of the first int64 source element (01 00 00 00) land in the first 4 bytes of dst[0], with the upper 4 bytes inheriting whatever was on the stack. dst[1] through dst[3] are fully uninitialized stack garbage.

Nothing in the surrounding text mentions the byte/element distinction. This is a sharp foot-gun on top of the FFI/ergonomic ones.

What the example presumably should show

A working version of the same snippet:

@[Import(lib: "msvcrt.dll")]
extern func memcpy(dst: *opaque, src: *opaque, n: uint) -> *opaque;

let src = [1u8, 2u8, 3u8, 4u8];                  // typed uint8 explicitly
var dst: uint8[4] = [0u8, 0u8, 0u8, 0u8];        // initialized; var because we'll write through .data
memcpy(dst.data, src.data, src.length * sizeof(uint8));   // bytes, not elements

(Or, if the goal is specifically to show calling memcpy on int64 data, leave [1, 2, 3, 4] and dst: int[4] and use src.length * sizeof(int) for the byte count.)

The point of the example is to illustrate slice.data as a *T that can be passed to extern functions — that lesson stands. The current snippet just buries it under three independent ways to be wrong.

Suggested fix

Two halves:

For slices.md:109-119 — rewrite the snippet so it actually works. Pick one of the working forms above. Add a sentence noting that slice.length is in elements, multiply by sizeof(T) for byte counts. Either fix the let dstvar dst issue here too, or wait for the broader doc sweep.

For extern.md — this is the bigger gap. The page should cover:

  • How to bind an extern declaration to a specific library (@[Import(lib: "msvcrt.dll")] and friends).
  • What the default behavior is when no @[Import] is given (does the compiler search system libs? error at link time? something else?).
  • Per-platform examples for the common targets (Windows kernel32/msvcrt, POSIX libc, etc.).
  • Whether there's a way to declare extern for non-DLL/SO inputs, e.g., static libraries or system call ABIs.

Right now the FFI doc is three lines of examples and one sentence about safety. A user who reads it has no idea how to actually link against anything.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions