OpenNHP Client Agent SDK Introduction
1 Client Agent SDK Introduction
1.1 Introduction
The OpenNHP Client Agent SDK is a standardized encapsulation of the OpenNHP Agent service. By integrating this SDK, applications can directly call the interface methods it provides to quickly achieve integration with OpenNHP. In different runtime environments, you only need to compile the SDK program into the corresponding system’s SDK file format:
| Operating System | Dynamic Library File |
|---|---|
| Linux | nhp-agent.so |
| Windows | nhp-agent.dll |
| MacOS | nhp-agent.dylib |
| Android | libnhpagent.so |
| IOS | nhpagent.xcframework |
1.2 SDK Development
OpenNHP provides sample SDK source code. The samples include methods that might be used, such as initializing the agent, starting cyclic knocking, stopping cyclic knocking, single knock, canceling a single knock, adding nhp-server services, setting client user information, and key registration. SDK developers can directly compile the SDK source code samples provided in the OpenNHP project into the corresponding SDK files for direct invocation, or refer to the SDK source code samples to complete custom SDK development.
SDK Sample Source Code: opennhp/endpoints/agent/main/export.go
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"unsafe"
"github.com/OpenNHP/opennhp/endpoints/agent"
"github.com/OpenNHP/opennhp/nhp/common"
"github.com/OpenNHP/opennhp/nhp/core"
)
var gAgentInstance *agent.UdpAgent
var gWorkingDir string
var gLogLevel int
func deepCopyCString(c_str *C.char) string {
if c_str == nil {
return ""
}
goStr := C.GoString(c_str)
return strings.Clone(goStr)
}
// Release the memory of the string buffer generated by NHPSDK.
//
//export nhp_free_cstring
func nhp_free_cstring(ptr *C.char) {
C.free(unsafe.Pointer(ptr))
}
// Initialization of the nhp_agent instance working directory path:
// The configuration files to be read are located under workingdir/etc/,
// and log files will be generated under workingdir/logs/.
//
// Input:
// workingDir: the working directory path for the agent
// logLevel: 0: silent, 1: error, 2: info, 3: debug, 4: verbose
//
// Return:
// Whether agent instance has been initialized successfully.
//
//export nhp_agent_init
func nhp_agent_init(workingDir *C.char, logLevel C.int) bool {
if gAgentInstance != nil {
return true
}
gAgentInstance = &agent.UdpAgent{}
err := gAgentInstance.Start(deepCopyCString(workingDir), int(logLevel))
if err != nil {
return false
}
return true
}
// Synchronously stop and release nhp_agent.
//
//export nhp_agent_close
func nhp_agent_close() {
if gAgentInstance == nil {
return
}
gAgentInstance.Stop()
gAgentInstance = nil
}
// Read the user information, resource information, server information,
// and other configuration files written under workingdir/etc,
// and asynchronously start the loop knocking thread.
//
// Input: None
//
// Return:
// -1: Uninitialized error
// >=0: The number of resources requested to knock by the knocking thread at the time of the call
//
// (knocking resources will be synchronized with changes in the configuration in workingdir/etc/resource.toml).
//
//export nhp_agent_knockloop_start
func nhp_agent_knockloop_start() C.int {
if gAgentInstance == nil {
return -1
}
count := gAgentInstance.StartKnockLoop()
return C.int(count)
}
// Synchronously stop the loop, knock-on sub thread.
//
//export nhp_agent_knockloop_stop
func nhp_agent_knockloop_stop() {
if gAgentInstance == nil {
return
}
gAgentInstance.StopKnockLoop()
}
// Setting agent's represented user information
//
// Input:
// userId: User identification (optional, but not recommended to be empty)
// devId: Device identification (optional)
// orgId: Organization or company identification (optional)
// userData: Additional fields required to interface with backend services (json format string, optional)
//
// Return:
// Whether the user information is set successfully
//
//export nhp_agent_set_knock_user
func nhp_agent_set_knock_user(userId *C.char, devId *C.char, orgId *C.char, userData *C.char) bool {
if gAgentInstance == nil {
return false
}
jsonStr := deepCopyCString(userData)
var data map[string]any
if len(jsonStr) > 0 {
err := json.Unmarshal([]byte(jsonStr), &data)
if err != nil {
return false
}
}
gAgentInstance.SetDeviceId(deepCopyCString(devId))
gAgentInstance.SetKnockUser(deepCopyCString(userId), deepCopyCString(orgId), data)
return true
}
// Add an NHP server information to the agent for use in knocking on the door
// (the agent can initiate different knocking requests to multiple NHP servers).
//
// Input:
// pubkey: Public key of the NHP server
// ip: IP address of the NHP server
// host: Domain name of the NHP server (if a domain name is set, the ip item is optional)
// port: Port number for the NHP server to operate (if set to 0, the default port 62206 will be used)
// expire: Expiration time of the NHP server's public key (in epoch seconds, set to 0 for permanent)
//
// Return:
// Whether the server information has been successfully added.
//
//export nhp_agent_add_server
func nhp_agent_add_server(pubkey *C.char, ip *C.char, host *C.char, port C.int, expire int64) bool {
if gAgentInstance == nil {
return false
}
if pubkey == nil || (ip == nil && host == nil) {
return false
}
serverPort := int(port)
if serverPort == 0 {
serverPort = 62206 // use default server listening port
}
serverPeer := &core.UdpPeer{
Type: core.NHP_SERVER,
PubKeyBase64: deepCopyCString(pubkey),
Ip: deepCopyCString(ip),
Port: serverPort,
Hostname: deepCopyCString(host),
ExpireTime: expire,
}
gAgentInstance.AddServer(serverPeer)
return true
}
// Delete NHP server information from the agent
//
// Input:
// pubkey: NHP server public key
//
//export nhp_agent_remove_server
func nhp_agent_remove_server(pubkey *C.char) {
if gAgentInstance == nil {
return
}
if pubkey == nil {
return
}
gAgentInstance.RemoveServer(deepCopyCString(pubkey))
}
// Please add a resource information for the agent to use for knocking on the door
// (the agent can initiate a knock-on request for different resources)
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the resource information has been added successfully
//
//export nhp_agent_add_resource
func nhp_agent_add_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) bool {
if gAgentInstance == nil {
return false
}
if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
return false
}
resource := &agent.KnockResource{
AuthServiceId: deepCopyCString(aspId),
ResourceId: deepCopyCString(resId),
ServerIp: deepCopyCString(serverIp),
ServerHostname: deepCopyCString(serverHostname),
ServerPort: int(serverPort),
}
err := gAgentInstance.AddResource(resource)
return err == nil
}
// Delete resource information from the agent
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
//
//export nhp_agent_remove_resource
func nhp_agent_remove_resource(aspId *C.char, resId *C.char) {
if gAgentInstance == nil {
return
}
if aspId == nil || resId == nil {
return
}
gAgentInstance.RemoveResource(deepCopyCString(aspId), deepCopyCString(resId))
}
// The agent initiates a single knock on the door request to the server hosting the resource
//
// Input:
// aspId: Authentication service provider identifier
// resId: Resource identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Returns:
// The server's response message (json format string buffer pointer):
// "errCode": Error code (string, "0" indicates success)
// "errMsg": Error message (string)
// "resHost": Resource server address ("resHost": {"Server Name 1":"Server Hostname 1", "Server Name 2":"Server Hostname 2", ...})
// "opnTime": Door opening duration (integer, in seconds)
// "aspToken": Token generated after authentication by the ASP (optional)
// "agentAddr": Agent's IP address from the perspective of the NHP server
// "preActs": Pre-connection information related to the resource (optional)
// "redirectUrl": HTTP redirection link (optional)
//
// It is necessary to call nhp_agent_add_server before calling,
// to add the NHP server's public key, address, and other information to the agent
// The caller is responsible for calling nhp_free_cstring to release the returned char* pointer
//
//export nhp_agent_knock_resource
func nhp_agent_knock_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) *C.char {
ackMsg := &common.ServerKnockAckMsg{}
func() {
if gAgentInstance == nil {
ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
return
}
if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
ackMsg.ErrMsg = common.ErrInvalidInput.Error()
return
}
resource := &agent.KnockResource{
AuthServiceId: deepCopyCString(aspId),
ResourceId: deepCopyCString(resId),
ServerIp: deepCopyCString(serverIp),
ServerHostname: deepCopyCString(serverHostname),
ServerPort: int(serverPort),
}
peer := gAgentInstance.FindServerPeerFromResource(resource)
if peer == nil {
ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
return
}
target := &agent.KnockTarget{
KnockResource: *resource,
ServerPeer: peer,
}
ackMsg, _ = gAgentInstance.Knock(target)
}()
bytes, _ := json.Marshal(ackMsg)
ret := C.CString(string(bytes))
return ret
}
// The agent explicitly informs the NHP server to exit its access permission to the resource.
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the exit was successful
//
// It is necessary to call nhp_agent_add_server before calling, to add the NHP server's public key, address, and other information to the agent.
//
//export nhp_agent_exit_resource
func nhp_agent_exit_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) bool {
var err error
ackMsg := &common.ServerKnockAckMsg{}
func() {
if gAgentInstance == nil {
ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
err = common.ErrNoAgentInstance
return
}
if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
ackMsg.ErrMsg = common.ErrInvalidInput.Error()
err = common.ErrInvalidInput
return
}
resource := &agent.KnockResource{
AuthServiceId: deepCopyCString(aspId),
ResourceId: deepCopyCString(resId),
ServerIp: deepCopyCString(serverIp),
ServerHostname: deepCopyCString(serverHostname),
ServerPort: int(serverPort),
}
peer := gAgentInstance.FindServerPeerFromResource(resource)
if peer == nil {
ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
err = common.ErrKnockServerNotFound
return
}
target := &agent.KnockTarget{
KnockResource: *resource,
ServerPeer: peer,
}
ackMsg, err = gAgentInstance.ExitKnockRequest(target)
}()
return err == nil
}
// cipherType: 0-curve25519; 1-sm2
// result: "privatekey"|"publickey"
// caller is responsible to free the returned char* pointer
//
//export nhp_generate_keys
func nhp_generate_keys(cipherType C.int) *C.char {
var e core.Ecdh
switch core.EccTypeEnum(cipherType) {
case core.ECC_SM2:
e = core.NewECDH(core.ECC_SM2)
case core.ECC_CURVE25519:
fallthrough
default:
e = core.NewECDH(core.ECC_CURVE25519)
}
pub := e.PublicKeyBase64()
priv := e.PrivateKeyBase64()
res := fmt.Sprintf("%s|%s", priv, pub)
pRes := C.CString(res)
return pRes
}
// cipherType: 0-curve25519; 1-sm2
// privateBase64: private key in base64 format
// result: "publickey"
// caller is responsible to free the returned char* pointer
//
//export nhp_privkey_to_pubkey
func nhp_privkey_to_pubkey(cipherType C.int, privateBase64 *C.char) *C.char {
privKey := deepCopyCString(privateBase64)
privKeyBytes, err := base64.StdEncoding.DecodeString(privKey)
if err != nil {
return nil
}
e := core.ECDHFromKey(core.EccTypeEnum(cipherType), privKeyBytes)
if e == nil {
return nil
}
pub := e.PublicKeyBase64()
pPub := C.CString(pub)
return pPub
}
2 Client Agent SDK Adaptation
2.1 Desktop SDK
2.1.1 Windows
2.1.1.1 Environment Preparation
Set up the compilation environment for Windows by referring to the Windows section in the System requirement chapter of Build OpenNHP Source Code.
2.1.1.2 Compiling the SDK
Method 1:Run the BAT file in the code root directory
build.bat
(Note: If an error occurs during the compilation process under windows, try this compilation method: In the Visual Studio developer command prompt for VS command window, switch to the project directory and execute the./build.batcommand)Method 2: Command to compile the .dll file for the SDK separately:
Navigate to the opennhp/endpoints/agent/main/ directory and execute:
go build -trimpath -buildmode=c-shared -ldflags '-s -w' -v -o nhp-agent.dll main.go export.go(Note: Because export.go does not contain a main method, main.go is included in the build command. For custom SDK code files that include a main method, the build command only needs the SDK code file and does not need to include main.go.)
2.1.1.3 SDK Adaptation
java
Java programs can call SDK methods using JNA:
OpennhpLibrary interface loads the OpenNHP agent SDK
package org.example; import com.sun.jna.Library; import com.sun.jna.Native; /** * OpenNHP agent sdk interface * * @author haochangjiu * @version JDK 8 * @className OpennhpLibrary * @date 2025/10/27 */ public interface OpennhpLibrary extends Library { // load OpenNHP agent sdk OpennhpLibrary INSTANCE = Native.load("nhp-agent", OpennhpLibrary.class); /** * @description Initialization of the nhp_agent instance working directory path: * The configuration files to be read are located under workingdir/etc/, * and log files will be generated under workingdir/logs/. * @param workingDir: the working directory path for the agent * @param logLevel: 0: silent, 1: error, 2: info, 3: debug, 4: verbose * return boolean Whether agent instance has been initialized successfully. * @return boolean * @author haochangjiu * @date 2025/10/27 * {@link boolean} */ boolean nhp_agent_init(String workingDir, int logLevel); /** * @description Synchronously stop and release nhp_agent. * @author haochangjiu * @date 2025/10/27 */ void nhp_agent_close(); /** * @description Read the user information, resource information, server information, * and other configuration files written under workingdir/etc, * and asynchronously start the loop knocking thread. * @return int * @author haochangjiu * @date 2025/10/27 * {@link int} */ int nhp_agent_knockloop_start(); /** * @description Synchronously stop the loop, knock-on sub thread * @author hangchangjiu * @date 2025/10/27 */ void nhp_agent_knockloop_stop(); }Application main entry, calling the SDK
package org.example; import java.util.Scanner; /** * Application for calling the OpenNHP agent SDK * * @author haochangjiu * @version JDK 8 * @className App * @date 2025/10/27 */ public class App { public static void main(String[] args) throws Exception { // Initialize and start the OpenNHP agent SDK service boolean initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init("D:\\console-workspace\\opennhp-knock", 3); if (!initFlag) { System.out.println("NHP Agent init failed"); System.exit(0); } // Invoke methods in the OpenNHP agent SDK via input commands Scanner scanner = new Scanner(System.in); while (true) { System.out.print("> "); if (scanner.hasNextLine()) { String input = scanner.nextLine().trim(); if ("knock".equalsIgnoreCase(input)) { System.out.println("start the loop knocking thread..."); OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start(); } else if ("cancel".equalsIgnoreCase(input)) { System.out.println("stop the loop knocking thread..."); OpennhpLibrary.INSTANCE.nhp_agent_knockloop_stop(); } else if ("exit".equalsIgnoreCase(input)) { System.out.println("exit nhp agent service..."); OpennhpLibrary.INSTANCE.nhp_agent_close(); break; } else { System.out.println("invalid input"); } } } scanner.close(); } }
c/c++
C/C++ programs can refer to the sample SDK calling program in the project opennhp/endpoints/agent/sdkdemo/nhp-agent-demo.c to integrate the client agent SDK.
#include <stdio.h> #include <unistd.h> #include "nhp-agent.h" int main() { // Initialize nhp_agent, only one nhp_agent singleton is allowed per process. nhp_agent_init(".", 3); // Set the user information for the knock-on-the-door feature. nhp_agent_set_knock_user("zengl", NULL, NULL, NULL); // Set NHP server information // If there is already a configuration file for the server, the call to nhp_agent_add_server can be omitted // Timestamp date is visible at https://unixtime.org/ nhp_agent_add_server("replace_with_actual_publickeybase64", "192.168.1.66", NULL, 62206, 1748908471); // Send a request to the server to access the resource example/demo, and return information in the form of a JSON format string // Note: The resource information here is an independent input, and is unrelated to the resource information saved in the configuration file char *ret = nhp_agent_knock_resource("example", "demo", "192.168.1.66", NULL, 62206); printf("knock return: %s\n", ret); // Immediately close the agent's access to the example/demo resources, // if not invoked, access permission will automatically close after the door opening duration has passed. nhp_agent_exit_resource("example", "demo", "192.168.1.66", NULL, 62206); // Turn off and release nhp_agent. nhp_agent_close(); return 0; }python
Use Python’s standard ctypes library to integrate the SDK.
import ctypes from time import sleep # Windows nhp_agent = ctypes.CDLL('nhp-agent.dll') # Linux # mylib = ctypes.CDLL('./nhp-agent.so') # macOS # mylib = ctypes.CDLL('./nhp-agent.dylib') nhp_agent.nhp_agent_init.argtypes = [ctypes.c_char_p, ctypes.c_int] nhp_agent.nhp_agent_init.restype = ctypes.c_bool nhp_agent.nhp_agent_init.restype = ctypes.c_int if __name__ == '__main__': flag = nhp_agent.nhp_agent_init(ctypes.c_char_p(b"D:\\nhpagent"),3) if flag: print("nhp-agent init success") else: print("nhp-agent init failed") # start the loop knocking thread status = nhp_agent.nhp_agent_knockloop_start() if status >= 0: print("nhp-agent knockloop success") # Delay between calls sleep(30) else: print("nhp-agent knockloop failed") # stop nhp_agent nhp_agent.nhp_agent_close()Other Languages
Other development languages (C#, Rust, Go, Nodejs) can adapt the SDK according to their unique methods for calling SDK files. Among them, Go can also introduce the source code of the agent part from OpenNHP to adapt to OpenNHP without developing an SDK.
2.1.2 Linux
2.1.2.1 Environment Preparation
Set up the compilation environment for Linux by referring to the Linux section in the System requirement chapter of Build OpenNHP Source Code.
2.1.2.2 Compiling the SDK
Method 1: Run the script in the project root directory.
makeMethod 2: Command to compile the .so file for the SDK separately:
Navigate to the opennhp/endpoints/agent/main/ directory and execute:
go build -trimpath -buildmode=c-shared -ldflags '-s -w' -v -o nhp-agent.so main.go export.go(Note: Because export.go does not contain a main method, main.go is included in the build command. For custom SDK code files that include a main method, the build command only needs the SDK code file and does not need to include main.go.)
2.1.2.3 SDK Adaptation
The SDK adaptation on Linux is the same as on Windows. Refer to section 2.1.1.3 for the code.
(Note: Ensure the program can normally load the SDK’s .so file.)
2.1.3 MacOS
2.1.3.1 Environment Preparation
Set up the compilation environment for MacOS by referring to the MacOS section in the System requirement chapter of Build OpenNHP Source Code.
2.1.3.2 Compiling the SDK
The SDK compiled via the make command is a .so file, but the dynamic library file format on MacOS is .dylib. Therefore, SDK compilation needs to be done separately.
Navigate to the opennhp/endpoints/agent/main/ directory and execute the build command:
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -buildmode=c-shared -o nhp-agent.dylib main.go export.go
(Note: Because export.go does not contain a main method, main.go is included in the build command. For custom SDK code files that include a main method, the build command only needs the SDK code file and does not need to include main.go.)
2.1.3.3 SDK Adaptation
The SDK adaptation on MacOS is the same as on Windows. Refer to section 2.1.1.3 for the code.
(Note: Ensure the program can normally load the SDK’s .dylib file.)
2.2 Mobile SDK
2.2.1 Android
2.2.1.1 Environment Preparation
Compile the Android client agent SDK on Linux. Set up the compilation environment by referring to the Linux section in the System requirement chapter of Build OpenNHP Source Code.
Android NDK Environment:
Download and install Android NDK.
wget https://dl.google.com/android/repository/android-ndk-r25b-linux.zip unzip android-ndk-r25b-linux.zipSet environment variables.
Edit the bashrc file.
vim ~/.bashrcAdd environment variables.
# Set NDK path (according to your actual installation path) export ANDROID_NDK_HOME=/opt/android-ndk-r25b/ export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64 # For arm64-v8a use aarch64 toolchain export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang export CXX=$TOOLCHAIN/bin/aarch64-linux-android21-clang++Make the configuration effective.
source ~/.bashrc
2.2.1.2 Compiling the SDK
Navigate to the opennhp/endpoints/agent/main/ directory and execute the build command:
GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build -buildmode=c-shared -o libnhpagent.so main.go export.go
(Note: When an Android project loads .so files via JNA, it adds ‘lib’ to the front of the input .so file name. When compiling the SDK, the name should start with ‘lib’, e.g., libnhpagent.so.)
2.2.1.3 SDK Adaptation
Android Configuration (Applicable for both Kotlin and Java):
1.Add the following configuration in build.gradle (app):
Add under the
androidsection:sourceSets { main { jniLibs.srcDirs = ['src/main/jniLibs', 'libs'] } }Add the following dependencies under the
dependenciessection:// Note: It is recommended for Android to use an adapted JNA version, e.g., 5.13.0 or higher.
implementation 'net.java.dev.jna:jna:5.13.0@aar'// Permission request framework: https://github.com/getActivity/XXPermissions
implementation libs.xxpermissionsIn the libs.versions.toml file: Under
[versions], add:xxpermissions = "18.6"Under[libraries], add:xxpermissions = { module = "com.github.getActivity:XXPermissions", version.ref = "xxpermissions" }2.Add file storage read and write permissions in the AndroidManifest.xml file:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
Kotlin
Kotlin Sample Code for Android Application SDK Adaptation
package com.example.androidtestsoapp import android.os.Bundle import android.os.Environment import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.example.androidtestsoapp.ui.theme.AndroidTestSoAppTheme import com.hjq.permissions.Permission import com.hjq.permissions.XXPermissions import java.io.File class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { AndroidTestSoAppTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Greeting( name = "Android", modifier = Modifier.padding(innerPadding) ) } } } // Request permissions - read/write XXPermissions.with(this) .permission(Permission.WRITE_EXTERNAL_STORAGE) .permission(Permission.READ_MEDIA_IMAGES) .permission(Permission.READ_MEDIA_VIDEO) .permission(Permission.READ_MEDIA_AUDIO) .request { permissions, allGranted -> if (allGranted) { Log.d("MainActivity", "Permissions granted") performFileOperations() } else { Log.d("MainActivity", "Permissions not granted") } } } } /** * Need to place the nhp folder containing the etc folder in the phone's download folder * After reading the phone storage download directory, call OpennhpLibrary */ private fun performFileOperations() { // Read phone storage download directory val appDir = Environment.getExternalStorageDirectory().toString() + File.separator + "download" // Check if zero folder exists in download val file = File(appDir) if (!file.exists()) { Log.d("MainActivity", "Download folder does not exist") return } Log.d("MainActivity", "Download folder exists") val appDir1 = Environment.getExternalStorageDirectory().toString() + File.separator + "download" + File.separator + "nhp" // Check if nhp folder exists in download val file1 = File(appDir1) if (!file1.exists()) { Log.d("MainActivity", "nhp folder does not exist") return } val appDir2 = Environment.getExternalStorageDirectory().toString() + File.separator + "download" + File.separator + "nhp"+ File.separator + "etc" // Check if etc folder exists in download val file2 = File(appDir2) if (!file2.exists()) { Log.d("MainActivity", "Etc folder does not exist") return } val initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init(appDir1, 2) if (!initFlag) { println("NHP Agent init failed") return } println("start the loop knocking thread...") val flag:Int = OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start() // Print result if (flag > 0) { println("NHP Agent knockloop start success") } else { println("NHP Agent knockloop start failed") } } @Composable fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = "Hello $name!", modifier = modifier ) } @Preview(showBackground = true) @Composable fun GreetingPreview() { AndroidTestSoAppTheme { Greeting("Android") } }java
Create the OpennhpLibrary interface to load the OpenNHP agent SDK.
(Note: When introducing .so files in an Android project, ‘lib’ is added before the dynamic library file name. That is, the SDK name loaded in the code is ‘nhpagent’, but the actual SDK loaded by the program is the ‘libnhpagent.so’ file.)
package org.example; import com.sun.jna.Library; import com.sun.jna.Native; /** * OpenNHP agent sdk interface * * @author haochangjiu * @version JDK 8 * @className OpennhpLibrary * @date 2025/10/27 */ public interface OpennhpLibrary extends Library { // load OpenNHP agent sdk OpennhpLibrary INSTANCE = Native.load("nhpagent", OpennhpLibrary.class); /** * @description Initialization of the nhp_agent instance working directory path: * The configuration files to be read are located under workingdir/etc/, * and log files will be generated under workingdir/logs/. * @param workingDir: the working directory path for the agent * @param logLevel: 0: silent, 1: error, 2: info, 3: debug, 4: verbose * return boolean Whether agent instance has been initialized successfully. * @return boolean * @author haochangjiu * @date 2025/10/27 * {@link boolean} */ boolean nhp_agent_init(String workingDir, int logLevel); /** * @description Synchronously stop and release nhp_agent. * @author haochangjiu * @date 2025/10/27 */ void nhp_agent_close(); /** * @description Read the user information, resource information, server information, * and other configuration files written under workingdir/etc, * and asynchronously start the loop knocking thread. * @return int * @author haochangjiu * @date 2025/10/27 * {@link int} */ int nhp_agent_knockloop_start(); /** * @description Synchronously stop the loop, knock-on sub thread * @author hangchangjiu * @date 2025/10/27 */ void nhp_agent_knockloop_stop(); }Calling the SDK: In the sample, the configuration file etc folder is placed in the nhp directory under the phone’s download directory.
package org.example; import android.os.Bundle; import android.os.Environment; import android.util.Log; import androidx.appcompat.app.AppCompatActivity; import com.OpennhpLibrary; import com.fancy.zerotrust.R; import java.io.File; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Read the phone's storage download directory. String appDir = Environment.getExternalStorageDirectory() + File.separator + "download"; // Does the nhp directory exist in the downloads File file = new File(appDir); if (!file.exists()) { Log.d("MainActivity","download file not exist!"); return; } Log.d("MainActivity","download file exist!"); String appDir1 = Environment.getExternalStorageDirectory() + File.separator + "download"+ File.separator + "nhp"; boolean initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init(appDir1, 3); if (!initFlag) { System.out.println("NHP Agent init failed"); System.exit(0); } System.out.println("start the loop knocking thread..."); OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start(); } }
2.2.2 IOS
2.2.2.1 Environment Preparation
Compile the IOS client agent SDK on MacOS. Set up the compilation environment by referring to the MacOS section in the System requirement chapter of Build OpenNHP Source Code.
Ensure Xcode is installed. If not, install it from the App Store.
Install gomobile:
Install
go install golang.org/x/mobile/cmd/gomobile@latestInitialize
gomobile init
2.2.2.2 SDK Sample
When compiling the .xcframework file required for IOS, the names of the exported methods must start with a capital letter, and the parameter types must be standard Go language types, not C.int and C.char. Another important point is that the code cannot be under package main. Move the program to a newly created sdk path.
Modified code based on the export.go file in OpenNHP is as follows:
package sdk
import "C"
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/OpenNHP/opennhp/endpoints/agent"
"github.com/OpenNHP/opennhp/nhp/common"
"github.com/OpenNHP/opennhp/nhp/core"
)
var gAgentInstance *agent.UdpAgent
var gWorkingDir string
var gLogLevel int
// Initialization of the nhp_agent instance working directory path:
// The configuration files to be read are located under workingdir/etc/,
// and log files will be generated under workingdir/logs/.
//
// Input:
// workingDir: the working directory path for the agent
// logLevel: 0: silent, 1: error, 2: info, 3: debug, 4: verbose
//
// Return:
// Whether agent instance has been initialized successfully.
func NhpAgentInit(workingDir string, logLevel int) bool {
if gAgentInstance != nil {
return true
}
gAgentInstance = &agent.UdpAgent{}
err := gAgentInstance.Start(workingDir, logLevel)
if err != nil {
return false
}
return true
}
// Synchronously stop and release nhp_
func NhpAgentClose() {
if gAgentInstance == nil {
return
}
gAgentInstance.Stop()
gAgentInstance = nil
}
// Read the user information, resource information, server information,
// and other configuration files written under workingdir/etc,
// and asynchronously start the loop knocking thread.
//
// Input: None
//
// Return:
// -1: Uninitialized error
// >=0: The number of resources requested to knock by the knocking thread at the time of the call
//
// (knocking resources will be synchronized with changes in the configuration in workingdir/etc/resource.toml).
//
//export NhpAgentKnockloopStart
func NhpAgentKnockloopStart() int {
if gAgentInstance == nil {
return -1
}
count := gAgentInstance.StartKnockLoop()
return count
}
// Synchronously stop the loop, knock-on sub thread.
func NhpAgentKnockloopStop() {
if gAgentInstance == nil {
return
}
gAgentInstance.StopKnockLoop()
}
// Setting agent's represented user information
//
// Input:
// userId: User identification (optional, but not recommended to be empty)
// devId: Device identification (optional)
// orgId: Organization or company identification (optional)
// userData: Additional fields required to interface with backend services (json format string, optional)
//
// Return:
// Whether the user information is set successfully
func NhpAgentSetKnockUser(userId string, devId string, orgId string, userData string) bool {
if gAgentInstance == nil {
return false
}
var data map[string]any
if len(userData) > 0 {
err := json.Unmarshal([]byte(userData), &data)
if err != nil {
return false
}
}
gAgentInstance.SetDeviceId(devId)
gAgentInstance.SetKnockUser(userId, orgId, data)
return true
}
// Add an NHP server information to the agent for use in knocking on the door
// (the agent can initiate different knocking requests to multiple NHP servers).
//
// Input:
// pubkey: Public key of the NHP server
// ip: IP address of the NHP server
// host: Domain name of the NHP server (if a domain name is set, the ip item is optional)
// port: Port number for the NHP server to operate (if set to 0, the default port 62206 will be used)
// expire: Expiration time of the NHP server's public key (in epoch seconds, set to 0 for permanent)
//
// Return:
// Whether the server information has been successfully added.
func NhpAgentAddServer(pubkey string, ip string, host string, port int, expire int64) bool {
if gAgentInstance == nil {
return false
}
if len(pubkey) == 0 || (len(ip) == 0 && len(host) == 0) {
return false
}
serverPort := int(port)
if serverPort == 0 {
serverPort = 62206 // use default server listening port
}
serverPeer := &core.UdpPeer{
Type: core.NHP_SERVER,
PubKeyBase64: pubkey,
Ip: ip,
Port: serverPort,
Hostname: host,
ExpireTime: expire,
}
gAgentInstance.AddServer(serverPeer)
return true
}
// Delete NHP server information from the agent
//
// Input:
// pubkey: NHP server public key
func NhpAgentRemoveServer(pubkey string) {
if gAgentInstance == nil {
return
}
if len(pubkey) == 0 {
return
}
gAgentInstance.RemoveServer(pubkey)
}
// Please add a resource information for the agent to use for knocking on the door
// (the agent can initiate a knock-on request for different resources)
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the resource information has been added successfully
func NhpAgentAddResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) bool {
if gAgentInstance == nil {
return false
}
if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
return false
}
resource := &agent.KnockResource{
AuthServiceId: aspId,
ResourceId: resId,
ServerIp: serverIp,
ServerHostname: serverHostname,
ServerPort: serverPort,
}
err := gAgentInstance.AddResource(resource)
return err == nil
}
// Delete resource information from the agent
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
func NhpAgentRemoveResource(aspId string, resId string) {
if gAgentInstance == nil {
return
}
if len(aspId) == 0 || len(resId) == 0 {
return
}
gAgentInstance.RemoveResource(aspId, resId)
}
// The agent initiates a single knock on the door request to the server hosting the resource
//
// Input:
// aspId: Authentication service provider identifier
// resId: Resource identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Returns:
// The server's response message (json format string buffer pointer):
// "errCode": Error code (string, "0" indicates success)
// "errMsg": Error message (string)
// "resHost": Resource server address ("resHost": {"Server Name 1":"Server Hostname 1", "Server Name 2":"Server Hostname 2", ...})
// "opnTime": Door opening duration (integer, in seconds)
// "aspToken": Token generated after authentication by the ASP (optional)
// "agentAddr": Agent's IP address from the perspective of the NHP server
// "preActs": Pre-connection information related to the resource (optional)
// "redirectUrl": HTTP redirection link (optional)
//
// It is necessary to call NhpAgentAddServer before calling,
// to add the NHP server's public key, address, and other information to the agent
// The caller is responsible for calling NhpFreeCstring to release the returned char* pointer
func NhpAgentKnockResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) string {
ackMsg := &common.ServerKnockAckMsg{}
func() {
if gAgentInstance == nil {
ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
return
}
if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
ackMsg.ErrMsg = common.ErrInvalidInput.Error()
return
}
resource := &agent.KnockResource{
AuthServiceId: aspId,
ResourceId: resId,
ServerIp: serverIp,
ServerHostname: serverHostname,
ServerPort: serverPort,
}
peer := gAgentInstance.FindServerPeerFromResource(resource)
if peer == nil {
ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
return
}
target := &agent.KnockTarget{
KnockResource: *resource,
ServerPeer: peer,
}
ackMsg, _ = gAgentInstance.Knock(target)
}()
bytes, _ := json.Marshal(ackMsg)
return string(bytes)
}
// The agent explicitly informs the NHP server to exit its access permission to the resource.
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the exit was successful
//
// It is necessary to call NhpAgentAddServer before calling, to add the NHP server's public key, address, and other information to the
func NhpAgentExitResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) bool {
var err error
ackMsg := &common.ServerKnockAckMsg{}
func() {
if gAgentInstance == nil {
ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
err = common.ErrNoAgentInstance
return
}
if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
ackMsg.ErrMsg = common.ErrInvalidInput.Error()
err = common.ErrInvalidInput
return
}
resource := &agent.KnockResource{
AuthServiceId: aspId,
ResourceId: resId,
ServerIp: serverIp,
ServerHostname: serverHostname,
ServerPort: serverPort,
}
peer := gAgentInstance.FindServerPeerFromResource(resource)
if peer == nil {
ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
err = common.ErrKnockServerNotFound
return
}
target := &agent.KnockTarget{
KnockResource: *resource,
ServerPeer: peer,
}
ackMsg, err = gAgentInstance.ExitKnockRequest(target)
}()
return err == nil
}
// cipherType: 0-curve25519; 1-sm2
// result: "privatekey"|"publickey"
// caller is responsible to free the returned char* pointer
//
//export NhpGenerateKeys
func NhpGenerateKeys(cipherType int) string {
var e core.Ecdh
switch core.EccTypeEnum(cipherType) {
case core.ECC_SM2:
e = core.NewECDH(core.ECC_SM2)
case core.ECC_CURVE25519:
fallthrough
default:
e = core.NewECDH(core.ECC_CURVE25519)
}
pub := e.PublicKeyBase64()
priv := e.PrivateKeyBase64()
res := fmt.Sprintf("%s|%s", priv, pub)
return res
}
// cipherType: 0-curve25519; 1-sm2
// privateBase64: private key in base64 format
// result: "publickey"
// caller is responsible to free the returned char* pointer
//
//export NhpPrivkeyToPubkey
func NhpPrivkeyToPubkey(cipherType int, privateBase64 string) string {
privKey := privateBase64
privKeyBytes, err := base64.StdEncoding.DecodeString(privKey)
if err != nil {
return ""
}
e := core.ECDHFromKey(core.EccTypeEnum(cipherType), privKeyBytes)
if e == nil {
return ""
}
pub := e.PublicKeyBase64()
return pub
}
2.2.2.3 Compiling the SDK
Navigate to the opennhp/endpoints/agent/sdk/ directory and execute the build command.(Note: The re-edited sdk source code files are placed under opennhp/endpoints/agent/sdk/)
gomobile bind -target ios -o nhpagent.xcframework .
2.2.2.4 SDK Adaptation
- Objective-C
- FileCopyManager.h: Declares methods to copy SDK configuration files to the sandbox.
// // FileCopyManager.h // TestXCFramework // // Created by haochangjiu on 2025/10/30. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface FileCopyManager : NSObject /// Copy the specified file(s) to the etc and certs directories in the application's home directory + (void)copyFilesToSandboxEtc; @end NS_ASSUME_NONNULL_END - FileCopyManager.m: Implementation of FileCopyManager.h.
// // FileCopyManager.m // TestXCFramework // // Created by haochangjiu on 2025/10/30. // #import "FileCopyManager.h" #import <Foundation/Foundation.h> @implementation FileCopyManager /// Copy the specified file(s) to the etc and certs directories in the application's home directory + (void)copyFilesToSandboxEtc { // 1. Retrieve the sandboxed Documents directory NSArray *documentsURLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; NSURL *documentsURL = [documentsURLs firstObject]; if (!documentsURL) { NSLog(@"Failed to retrieve Documents directory"); return; } // 2. Define paths for etc and certs directories within the sandbox NSURL *etcURL = [documentsURL URLByAppendingPathComponent:@"etc"]; NSURL *certsURL = [etcURL URLByAppendingPathComponent:@"certs"]; // 3. Create etc and certs directories (if they don't exist) [self createDirectoryIfNotExists:etcURL]; [self createDirectoryIfNotExists:certsURL]; // 4. Copy toml files to the etc directory NSArray *tomlFiles = @[@"server.toml", @"config.toml", @"dhp.toml", @"resource.toml"]; for (NSString *fileName in tomlFiles) { [self copyFileFromBundle:fileName toDestinationURL:etcURL]; } // 5. Copy certificate files to the etc/certs directory NSArray *certFiles = @[@"server.crt", @"server.key"]; for (NSString *fileName in certFiles) { [self copyFileFromBundle:fileName toDestinationURL:certsURL]; } } /// Create directory if it does not exist + (void)createDirectoryIfNotExists:(NSURL *)directoryURL { NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:directoryURL.path]) { NSError *error; BOOL success = [fileManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:nil error:&error]; if (success) { NSLog(@"Directory created successfully: %@", directoryURL.path); } else { NSLog(@"Failed to create directory: %@, error: %@", directoryURL.path, error.localizedDescription); } } else { NSLog(@"Directory already exists: %@", directoryURL.path); } } /// Copy file from Bundle to destination path + (void)copyFileFromBundle:(NSString *)fileName toDestinationURL:(NSURL *)destinationURL { // Get the file path in the Bundle NSURL *sourceURL = [[NSBundle mainBundle] URLForResource:[fileName stringByDeletingPathExtension] withExtension:[fileName pathExtension]]; if (!sourceURL) { NSLog(@"File not found in Bundle: %@", fileName); return; } // Destination file path (destination directory + file name) NSURL *destFileURL = [destinationURL URLByAppendingPathComponent:fileName]; // Copy file (if it doesn't exist) NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:destFileURL.path]) { NSError *error; BOOL success = [fileManager copyItemAtURL:sourceURL toURL:destFileURL error:&error]; if (success) { NSLog(@"File copied successfully: %@ -> %@", fileName, destFileURL.path); } else { NSLog(@"File copy failed: %@, error: %@", fileName, error.localizedDescription); } } else { NSLog(@"File already exists: %@", destFileURL.path); } } @end - ViewController.m: Program main entry, calling SDK methods.
// // ViewController.m // TestXCFramework // // Created by haochangjiu on 2025/10/30. // #import "ViewController.h" #import <Nhpagent/Nhpagent.h> #import "FileCopyManager.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. // Invoke method to copy files from etc folder to sandbox etc directory [FileCopyManager copyFilesToSandboxEtc]; // Retrieve the sandbox target path (Documents), which is the parent directory of the etc folder NSArray *documentsURLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; NSURL *documentsURL = [documentsURLs firstObject]; if (!documentsURL) { NSLog(@"Error: Failed to read Documents directory"); } // Get the parent directory path of the etc folder NSString *etcPath = documentsURL.path; // SdkNhpAgentInit BOOL initFlag = SdkNhpAgentInit(etcPath, 3); if (!initFlag) { NSLog(@"NHP Agent init failed"); return; } // knockloop_start long value = SdkNhpAgentKnockloopStart(); NSLog(@"SdkNhpAgentKnockloopStart value : %ld", value); } @end
- FileCopyManager.h: Declares methods to copy SDK configuration files to the sandbox.
- Swift
FileCopyManager.swift: Methods to copy SDK configuration files to the sandbox.
// // FileCopyManager.swift // TestXCFrameworkSwift // // Created by haochangjiu on 2025/10/30. // import UIKit import Foundation class FileCopyManager { /// Copy specified files to the etc and certs directories in the sandbox static func copyFilesToSandboxEtc() { // 1. Get the Documents directory in the sandbox guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { print("Failed to get Documents directory") return } // 2. Define paths for etc and certs directories in the sandbox let etcURL = documentsURL.appendingPathComponent("etc") let certsURL = etcURL.appendingPathComponent("certs") // 3. Create etc and certs directories (if they don't exist) createDirectoryIfNotExists(at: etcURL) createDirectoryIfNotExists(at: certsURL) // 4. Copy toml files to the etc directory let tomlFiles = ["server.toml", "config.toml", "dhp.toml", "resource.toml"] tomlFiles.forEach { fileName in copyFileFromBundle(fileName: fileName, to: etcURL) } // 5. Copy certificate files to the etc/certs directory let certFiles = ["server.crt", "server.key"] certFiles.forEach { fileName in copyFileFromBundle(fileName: fileName, to: certsURL) } } /// Create directory if it doesn't exist private static func createDirectoryIfNotExists(at url: URL) { let fileManager = FileManager.default guard !fileManager.fileExists(atPath: url.path) else { print("Directory already exists: \(url.path)") return } do { try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) print("Directory created successfully: \(url.path)") } catch { print("Failed to create directory: \(url.path), error: \(error.localizedDescription)") } } /// Copy file from Bundle to destination path private static func copyFileFromBundle(fileName: String, to destinationURL: URL) { // Split filename and extension (handling files with extensions) let fileNameWithoutExt = (fileName as NSString).deletingPathExtension let fileExt = (fileName as NSString).pathExtension // Get the file path in the Bundle guard let sourceURL = Bundle.main.url(forResource: fileNameWithoutExt, withExtension: fileExt) else { print("File not found in Bundle: \(fileName)") return } // Destination file path (destination directory + filename) let destFileURL = destinationURL.appendingPathComponent(fileName) let fileManager = FileManager.default // Copy file (if it doesn't exist) guard !fileManager.fileExists(atPath: destFileURL.path) else { print("File already exists: \(destFileURL.path)") return } do { try fileManager.copyItem(at: sourceURL, to: destFileURL) print("File copied successfully: \(fileName) -> \(destFileURL.path)") } catch { print("File copy failed: \(fileName), error: \(error.localizedDescription)") } } }ViewController.swift: Program main entry, calling SDK methods.
// // ViewController.swift // TestXCFrameworkSwift // // Created by haochangjiu on 2025/10/30. // import UIKit import Nhpagent class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // Call method to copy files from etc folder to sandbox etc directory FileCopyManager.copyFilesToSandboxEtc() // Retrieve the sandbox target path (Documents), which is the parent directory of the etc folder guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { print("Error: Failed to read Documents directory") return } // Get the parent directory path of the etc folder let etcPath: String = documentsURL.path // Call SdkNhpAgentInit for initialization let initFlag: Bool = SdkNhpAgentInit(etcPath, 3) if !initFlag { print("NHP Agent init failed") } // Call knockloop_start let value = SdkNhpAgentKnockloopStart() print("SdkNhpAgentKnockloopStart value: %ld", value) } }