Skip to content
23 changes: 23 additions & 0 deletions c/net/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# C networking bindings

This package binds `arpa/inet.h`, `net/if.h`, `netdb.h`, `netinet/in.h`, `sys/socket.h`, and `sys/un.h`, together with their required C types. Each target is generated as one common libc translation unit, then classified into the existing C packages, with unmodified llcppg commit `da227ff4aaf07718d7ec0dea62e23dab03ac57fb`.

| Target | Header source | Build constraint |
| --- | --- | --- |
| macOS arm64 | macOS 14.0 SDK | `darwin && arm64 && !ios` |
| Linux amd64 | Bootlin stable 2017.05, glibc 2.24 | `linux && amd64 && !android` |
| Linux arm64 | Bootlin stable 2017.05, glibc 2.24 | `linux && arm64 && !android` |

Linux support assumes glibc. Existing implementations remain selected for other targets.

Generated declarations use the existing public function names, including `Bind`, `Accept`, `GetHostByName`, `InetAddr`, `InetNtop`, `Getaddrinfo`, `Send`, and `Recv`. Duplicate `Native` declarations have been removed. `Bind` and `Accept` retain their existing `*SockaddrIn` address parameters, preserving IPv4 caller compatibility while using the corrected native address layouts. `Send` and `Recv` retain `c.SizeT` (`uintptr`) for their lengths. Existing string entry points retain `*c.Char`, including on Linux arm64. `legacy_native.go` retains the five untyped socket constants; `legacy_darwin_arm64.go` retains the four native byte-order declarations whose macOS headers expose macros. The root package `c.Char` and `c.Printf` remain unchanged.

Address types retain the existing field names for corresponding C fields. Linux address families use a 16-bit field without the BSD length byte, and Linux `AddrInfo` places `Addr` before `CanOnName`. `In6Addr.U6Addr` remains a byte array with native IPv6 alignment. `Iovec` aliases the shared `c.Iovec` definition from the root C package. The compatibility padding constants follow the native `SockaddrStorage`: `PAD1_SIZE`/`PAD2_SIZE` are 6/112 on macOS and 118/0 on Linux.

The directory `llcppg.cfg` contains only `Dir`, used by the existing local directory-dependency caller to locate prepared headers. It is dependency metadata, not an independent generation recipe. Generation uses the unified libc configurations followed by classification. Each target is kept in one `libc_<os>_<arch>.go` file. Generation inputs, scripts, and header caches remain local; `_generate` is not included. The `llcppg.pub` manifest maps types present in the classified outputs.

The kernel-only macOS `pfctlinput` entry point and the Linux implementation helpers `__h_errno_location` and `__cmsg_nxthdr` are excluded. Types needed to express native layouts, including anonymous union storage and its generated field accessors, remain available. Existing approved generation-error and unsupported-layout exclusions remain recorded in the local generation inputs. `Cmsghdr` contains only its fixed C header fields: 16 bytes with 8-byte alignment on Linux and 12 bytes with 4-byte alignment on macOS. The interface enumeration function is `IfNameindex`; its record is `IfNameindexT`. Linux resolver declarations link `libresolv`. This remains a selection of common native APIs rather than the entire libc API.

The tests compare every retained C function's parameter/result ABI against independently compiled native prototypes and take its address to require symbol resolution. They also check native type sizes, alignments and field offsets, constant values, global symbols and layouts, and generated union accessors. A source inventory check rejects missing coverage. Runtime tests exercise binary socket I/O, IPv4/IPv6 numeric conversion and resolution, interface enumeration/freeing, and TCP loopback using the existing `Bind`/`Accept` IPv4 signatures without pointer casts.

The independent native C link check passes for macOS. Linux excludes `bindresvport6` with explicit approval: glibc 2.24's header declares it, but neither its original libc nor the validation environment's libc exports it. macOS arm64 and Linux amd64/arm64 pass `llgo test -count=1 -v ./c/net` with GC and `nogc` using LLGo v1.0.8 (`c171abfa130108bc6451e01fcf15a8a3474eb0a7`). The macOS runs use LLD 23.1.3 with the compiler on LLVM 22. Tests use the root C bindings from dependent PR #45, which supplies `c.Iovec`. The glibc 2.24 and macOS 14 SDK header baselines do not establish minimum executable runtime versions.
199 changes: 199 additions & 0 deletions c/net/generated_native_llgo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//go:build llgo && ((darwin && arm64 && !ios) || (linux && (amd64 || arm64) && !android))

package net

import (
"github.com/goplus/lib/c"
"runtime"
"syscall"
"testing"
"unsafe"
)

type networkABISlot struct {
size, align uintptr
class int
}

func TestGeneratedSocketIO(t *testing.T) {
var pair [2]c.Int
if result := Socketpair(c.Int(AF_UNIX), c.Int(SOCK_STREAM), 0, &pair[0]); result != 0 {
t.Fatalf("socketpair = %d", result)
}
defer syscall.Close(int(pair[0]))
defer syscall.Close(int(pair[1]))
var kind c.Int
length := SocklenT(unsafe.Sizeof(kind))
if result := Getsockopt(pair[0], SOL_SOCKET, SO_TYPE, unsafe.Pointer(&kind), &length); result != 0 {
t.Fatalf("getsockopt = %d", result)
}
if kind != SOCK_STREAM || length != SocklenT(unsafe.Sizeof(kind)) {
t.Fatalf("socket type/length = %d/%d", kind, length)
}
input := [6]byte{0, 1, 127, 128, 254, 255}
var output [6]byte
for offset := 0; offset < len(input); {
n := Send(pair[0], unsafe.Pointer(&input[offset]), c.SizeT(len(input)-offset), 0)
if n <= 0 {
t.Fatalf("send = %d", n)
}
offset += int(n)
}
for offset := 0; offset < len(output); {
n := Recv(pair[1], unsafe.Pointer(&output[offset]), c.SizeT(len(output)-offset), 0)
if n <= 0 {
t.Fatalf("recv = %d", n)
}
offset += int(n)
}
if output != input {
t.Fatalf("socket data = %v, want %v", output, input)
}
t.Run("byte order and IPv4 rendering", func(t *testing.T) {
if Htons(0x1234) != 0x3412 || Ntohs(0x3412) != 0x1234 || Htonl(0x12345678) != 0x78563412 || Ntohl(0x78563412) != 0x12345678 {
t.Fatal("native byte order conversion failed")
}
addr := InAddr{Addr: InetAddr(c.Str("127.0.0.1"))}
if got := c.GoString((*c.Char)(unsafe.Pointer(addr.InetNtoa()))); got != "127.0.0.1" {
t.Fatalf("inet_ntoa = %q", got)
}
})
t.Run("IPv4 and IPv6 numeric addresses", func(t *testing.T) {
for _, tc := range []struct {
family c.Int
text string
}{{AF_INET, "127.0.0.1"}, {AF_INET6, "::1"}} {
t.Run(tc.text, func(t *testing.T) {
var address [16]byte
text := append([]byte(tc.text), 0)
source := (*c.Char)(unsafe.Pointer(&text[0]))
if result := InetPton(tc.family, (*nativeNetworkChar)(unsafe.Pointer(source)), unsafe.Pointer(&address[0])); result != 1 {
t.Fatalf("inet_pton = %d", result)
}
var rendered [64]c.Char
if InetNtop(tc.family, unsafe.Pointer(&address[0]), &rendered[0], SocklenT(len(rendered))) == nil {
t.Fatal("inet_ntop failed")
}
if got := c.GoString(&rendered[0]); got != tc.text {
t.Fatalf("inet_ntop = %q", got)
}
hints := AddrInfo{Flags: AI_NUMERICHOST | AI_NUMERICSERV, Family: tc.family, SockType: SOCK_STREAM}
var info *AddrInfo
if result := Getaddrinfo(source, c.Str("32123"), &hints, &info); result != 0 {
t.Fatalf("getaddrinfo = %d", result)
}
if info == nil {
t.Fatal("getaddrinfo returned nil")
}
defer Freeaddrinfo(info)
if info.Family != tc.family || info.Addr == nil {
t.Fatalf("addrinfo family/address = %d/%p", info.Family, info.Addr)
}
var host [64]nativeNetworkChar
var service [16]nativeNetworkChar
if result := info.Addr.Getnameinfo(info.AddrLen, &host[0], SocklenT(len(host)), &service[0], SocklenT(len(service)), NI_NUMERICHOST|NI_NUMERICSERV); result != 0 {
t.Fatalf("getnameinfo = %d", result)
}
if got := c.GoString((*c.Char)(unsafe.Pointer(&host[0]))); got != tc.text {
t.Fatalf("numeric host = %q", got)
}
if got := c.GoString((*c.Char)(unsafe.Pointer(&service[0]))); got != "32123" {
t.Fatalf("numeric service = %q", got)
}
})
}
if In6addrAny.U6Addr != [16]uint8{} || In6addrLoopback.U6Addr != [16]uint8{15: 1} {
t.Fatal("native IPv6 globals are incorrect")
}
})
t.Run("interface index lifecycle", func(t *testing.T) {
loopback := "lo"
if runtime.GOOS == "darwin" {
loopback = "lo0"
}
loopbackText := append([]byte(loopback), 0)
index := IfNametoindex((*nativeNetworkChar)(unsafe.Pointer(&loopbackText[0])))
if index == 0 {
t.Fatal("loopback interface index is zero")
}
var name [256]nativeNetworkChar
if IfIndextoname(index, &name[0]) == nil {
t.Fatal("if_indextoname failed")
}
if got := c.GoString((*c.Char)(unsafe.Pointer(&name[0]))); got != loopback {
t.Fatalf("interface name = %q", got)
}
interfaces := IfNameindex()
if interfaces == nil {
t.Fatal("if_nameindex failed")
}
defer interfaces.IfFreenameindex()
found := false
for i := 0; ; i++ {
entry := (*IfNameindexT)(unsafe.Add(unsafe.Pointer(interfaces), uintptr(i)*unsafe.Sizeof(*interfaces)))
if entry.IfIndex == 0 && entry.IfName == nil {
break
}
if entry.IfIndex == index && c.GoString((*c.Char)(unsafe.Pointer(entry.IfName))) == loopback {
found = true
}
}
if !found {
t.Fatal("loopback interface is absent from if_nameindex")
}
})
t.Run("TCP loopback IPv4 compatibility", func(t *testing.T) {
listener := Socket(AF_INET, SOCK_STREAM, 0)
if listener < 0 {
t.Fatal("listener socket failed")
}
defer syscall.Close(int(listener))
address := SockaddrIn{Family: SaFamilyT(AF_INET), Addr: InAddr{Addr: Htonl(0x7f000001)}}
if runtime.GOOS == "darwin" {
*(*uint8)(unsafe.Pointer(&address)) = uint8(unsafe.Sizeof(address))
}
if result := Bind(listener, &address, SocklenT(unsafe.Sizeof(address))); result != 0 {
t.Fatalf("bind = %d", result)
}
if result := Listen(listener, 1); result != 0 {
t.Fatalf("listen = %d", result)
}
length := SocklenT(unsafe.Sizeof(address))
if result := Getsockname(listener, (*SockAddr)(unsafe.Pointer(&address)), &length); result != 0 {
t.Fatalf("getsockname = %d", result)
}
if length != SocklenT(unsafe.Sizeof(address)) || address.Port == 0 {
t.Fatalf("bound address length/port = %d/%d", length, address.Port)
}
client := Socket(AF_INET, SOCK_STREAM, 0)
if client < 0 {
t.Fatal("client socket failed")
}
defer syscall.Close(int(client))
if result := Connect(client, (*SockAddr)(unsafe.Pointer(&address)), length); result != 0 {
t.Fatalf("connect = %d", result)
}
var peer SockaddrIn
peerLength := SocklenT(unsafe.Sizeof(peer))
accepted := Accept(listener, &peer, &peerLength)
if accepted < 0 {
t.Fatalf("accept = %d", accepted)
}
defer syscall.Close(int(accepted))
if peer.Family != SaFamilyT(AF_INET) || peerLength != SocklenT(unsafe.Sizeof(address)) {
t.Fatalf("peer family/length = %d/%d", peer.Family, peerLength)
}
input := byte(0xa5)
if n := Send(client, unsafe.Pointer(&input), 1, 0); n != 1 {
t.Fatalf("TCP send = %d", n)
}
var output byte
if n := Recv(accepted, unsafe.Pointer(&output), 1, 0); n != 1 || output != input {
t.Fatalf("TCP recv = %d/%x", n, output)
}
if result := Shutdown(client, SHUT_WR); result != 0 {
t.Fatalf("shutdown = %d", result)
}
})

}
20 changes: 20 additions & 0 deletions c/net/legacy_darwin_arm64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//go:build darwin && arm64 && !ios

package net

import (
"github.com/goplus/lib/c"
_ "unsafe"
)

//go:linkname Ntohs C.ntohs
func Ntohs(x uint16) uint16

//go:linkname Htons C.htons
func Htons(x uint16) uint16

//go:linkname Ntohl C.ntohl
func Ntohl(x c.Uint) c.Uint

//go:linkname Htonl C.htonl
func Htonl(x c.Uint) c.Uint
28 changes: 28 additions & 0 deletions c/net/legacy_native.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build (darwin && arm64 && !ios) || (linux && amd64 && !android) || (linux && arm64 && !android)

/*
* Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net

// Keep the existing untyped constants usable with Socket's c.Int parameter.
const (
SOCK_STREAM = 1
SOCK_DGRAM = 2
SOCK_RAW = 3
SOCK_RDM = 4
SOCK_SEQPACKET = 5
)
Loading
Loading