简体   繁体   中英

Use the sysctlbyname function within a Linux Swift Package

i am trying to add linux support to my swift package library for system info, but i don't know how i can access the sysctlbyname function on linux within a Swift package.

For all of it's detections the library relies on the sysctlbyname function which is easily accessible by importing Dariwn.sys.sysctl on Apple platforms, however i can't find any Swift ways to access that function on linux, despite the fact that you can access it in C by importing sys/sysctl.h on basically any unix platform.

So i was wondering how can access that function in my Swift library on linux and if it's possible to do it without having to use C or some other non-Swift stuff, also because i'd like to keep my code compatible with the Swift playgrounds app for apple systems, which doesn't support SPM libraries featuring C imports.

Just as a reference i leave here the part of the code responsible for interfacing with sysctlbyname in my project:


import Foundation

#if os(Linux)
import Glibc //? not sure about where i can find `sysctlbyname` in linux without using C headers
#else
import Darwin.sys.sysctl
#endif

///Generic protocol to allow easy fetching of values out of `sysctlbyname`
public protocol SysctlFetch{
    static var namePrefix: String {get}
}

public extension SysctlFetch{
    
    ///Gets a `String` from the `sysctlbyname` function
    static func getString(_ valueName: String) -> String?{
        
        var size: size_t = 0
        
        let name = namePrefix + valueName
        
        var res = sysctlbyname(name, nil, &size, nil, 0)
        
        if res != 0 {
            return nil
        }
        
        var ret = [CChar].init(repeating: 0, count: size + 1)
        
        res = sysctlbyname(name, &ret, &size, nil, 0)
        
        return res == 0 ? String(cString: ret) : nil
    }
    
    ///Gets an Integer value from the `sysctlbyname` function
    static func getInteger<T: FixedWidthInteger>(_ valueName: String) -> T?{
        var ret = T()
        
        var size = MemoryLayout.size(ofValue: ret)
        
        let res = sysctlbyname(namePrefix + valueName, &ret, &size, nil, 0)
        
        return res == 0 ? ret : nil
    }
    
    ///Gets a `Bool` value from the `sysctlbyname` function
    static func getBool(_ valueName: String) -> Bool?{
        guard let res: Int32 = getInteger(valueName) else{
            return nil
        }
        
        return res == 1
    }
    
}

And an example of how it's used in the code (of curse it's used to retrive much more stuff):

    ///Kernel info
    final class KernelInfo: SysctlFetch{
        
        static var namePrefix: String{
            #if os(Linux)
                return "kernel."
            #else
                return "kern."
            #endif
        }
        
        ///The os kernel name
        static var ostype: String?{
            return Self.getString("ostype")
        }

        /* Other static vars here */

    }


So one can add C, C++ or Objective-C targets to a Swift package so it's possible to import the needed system headers, and then create some wrapper functions, that makes what is needed, accessible to Swift, but this breaks Swift playgrounds app development compatibility, since that support Swift-only targets (a possible workaround is to put the C/C++ target in a separate swift package to use it as a dependecy conditionally just for linux, for more details see the relative swift package documentation).

So adding a C/C++ target could have solved the problem, BUT the issue is that in the Linux kernel version 5.5 and onwards the sysctl functions have been deprecated and even on the older kernels they weren't available on all the cpu architectures Linux supports, and so on a computer running a recent kernel or some particular non-x86 cpu architecture, such Swift package would not have been built successfully.

The current way to access the information that used to be provided by the sysctl functions is to read it directly from the file system inside the /proc/sys directory and it works on all supported cpu architectures, and it's were the sysctl command line utility gets that data.

So only on linux the code have to modified like this, to successfully gather that data on all platforms:


import Foundation

#if os(Linux)
import Glibc //? not sure about where i can find `sysctlbyname` in linux without using C headers
#else
import Darwin.sys.sysctl
#endif

///Generic protocol to allow easy fetching of values out of `sysctlbyname`
public protocol SysctlFetch{
    static var namePrefix: String {get}
}

public extension SysctlFetch{
    
#if !os(Linux)
    ///Gets a `String` from the `sysctlbyname` function
    static func getString(_ valueName: String) -> String?{
        
        var size: size_t = 0
        
        let name = namePrefix + valueName
        
        var res = sysctlbyname(name, nil, &size, nil, 0)
        
        if res != 0 {
            return nil
        }
        
        var ret = [CChar].init(repeating: 0, count: size + 1)
        
        res = sysctlbyname(name, &ret, &size, nil, 0)
        
        return res == 0 ? String(cString: ret) : nil
    }
    
    ///Gets an Integer value from the `sysctlbyname` function
    static func getInteger<T: FixedWidthInteger>(_ valueName: String) -> T?{
        var ret = T()
        
        var size = MemoryLayout.size(ofValue: ret)
        
        let res = sysctlbyname(namePrefix + valueName, &ret, &size, nil, 0)
        
        return res == 0 ? ret : nil
    }
#else
    ///Gets a `String` from `/proc/sys`
    static func getString(_ valueName: String) -> String?{
        
        let path = "/proc/sys/" + (namePrefix + valueName).replacingOccurrences(of: ".", with: "/")

        var contents = ""
        
        do{
            contents = try String(contentsOfFile: path)
        }catch let err{
            return nil
        }
        
        if contents.last == "\n"{
            contents.removeLast()
        }
        
        return contents
    }
    
    ///Gets an Integer value from `/proc/sys`
    static func getInteger<T: FixedWidthInteger>(_ valueName: String) -> T?{
        guard let str = getString(valueName) else { return nil }
        return T(str)
    }
#endif
    
    ///Gets a `Bool` value from the `sysctlbyname` function
    static func getBool(_ valueName: String) -> Bool?{
        guard let res: Int32 = getInteger(valueName) else{
            return nil
        }
        
        return res == 1
    }
    
}

So at the end i figured it out on my own, i hope this can be useful to anyone having to do the same thing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM